@starci/hfs 4.3.0 → 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 (515) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/db/migrate.mjs +361 -0
  3. package/db/post-cutover.mjs +136 -0
  4. package/emit/db-types.mjs +10 -1
  5. package/lint/run.mjs +1 -1
  6. package/package.json +4 -1
  7. package/release/gate.mjs +298 -0
  8. package/release/index.mjs +103 -0
  9. package/release/push.mjs +110 -0
  10. package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
  11. package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
  12. package/runtime/knowledge/hfs/rules.yaml +3401 -152
  13. package/runtime/knowledge/hfs/slots.yaml +344 -62
  14. package/runtime/knowledge/patterns/be/api.yaml +32 -5
  15. package/runtime/knowledge/patterns/be/cli.yaml +14 -4
  16. package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
  17. package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
  18. package/runtime/knowledge/patterns/be/projections.yaml +4 -2
  19. package/runtime/knowledge/patterns/be/queues.yaml +11 -3
  20. package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
  21. package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
  22. package/runtime/knowledge/patterns/be/saga.yaml +20 -3
  23. package/runtime/knowledge/patterns/be/webhooks.yaml +23 -4
  24. package/runtime/knowledge/patterns/fe/folder.yaml +1 -0
  25. package/runtime/knowledge/sonar-gate.yaml +1 -1
  26. package/runtime/modules/kernel/failure-codes.yaml +3113 -43
  27. package/runtime/modules/models/runtimes.yaml +1 -1
  28. package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
  29. package/runtime/packages/cli/src/catalog.generated.mjs +14104 -0
  30. package/runtime/packages/cli/src/validate-args.mjs +213 -0
  31. package/runtime/scripts/api/git/add.mjs +5 -0
  32. package/runtime/scripts/api/git/init.mjs +7 -0
  33. package/runtime/scripts/api/git/lib.mjs +3 -0
  34. package/runtime/scripts/api/git/tag.mjs +5 -0
  35. package/runtime/scripts/api/node/lib.mjs +20 -0
  36. package/runtime/scripts/api/node/run-node.mjs +6 -0
  37. package/runtime/scripts/api/sops/lib.mjs +41 -0
  38. package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
  39. package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
  40. package/runtime/scripts/api/supabase/database-query.mjs +5 -0
  41. package/runtime/scripts/api/supabase/lib.mjs +44 -0
  42. package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
  43. package/runtime/scripts/hfs/app-wiring.mjs +201 -0
  44. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +1 -8
  45. package/runtime/scripts/hfs/architecture/backend.mjs +2 -7
  46. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
  47. package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
  48. package/runtime/scripts/hfs/architecture/dead-exports.mjs +9 -41
  49. package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
  50. package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
  51. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
  52. package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
  53. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +28 -13
  54. package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
  55. package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
  56. package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
  57. package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
  58. package/runtime/scripts/hfs/architecture/register-once.mjs +2 -2
  59. package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
  60. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +211 -11
  61. package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -1
  62. package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
  63. package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
  64. package/runtime/scripts/hfs/architecture/tiers.mjs +3 -3
  65. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +8 -2
  66. package/runtime/scripts/hfs/check-results.mjs +28 -0
  67. package/runtime/scripts/hfs/check.mjs +46 -47
  68. package/runtime/scripts/hfs/coverage-from.mjs +70 -0
  69. package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
  70. package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
  71. package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
  72. package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
  73. package/runtime/scripts/hfs/doc-commands.mjs +367 -0
  74. package/runtime/scripts/hfs/edition-slots.mjs +2 -2
  75. package/runtime/scripts/hfs/example-secrets.mjs +158 -0
  76. package/runtime/scripts/hfs/manifest-shape.mjs +67 -7
  77. package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
  78. package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
  79. package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
  80. package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
  81. package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
  82. package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
  83. package/runtime/scripts/hfs/rules/assets.mjs +79 -0
  84. package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
  85. package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
  86. package/runtime/scripts/hfs/rules/contract.mjs +66 -5
  87. package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
  88. package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
  89. package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
  90. package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
  91. package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
  92. package/runtime/scripts/hfs/rules/database-migrations.mjs +286 -7
  93. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
  94. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
  95. package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
  96. package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
  97. package/runtime/scripts/hfs/rules/database.mjs +9 -0
  98. package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
  99. package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
  100. package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
  101. package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
  102. package/runtime/scripts/hfs/rules/docker.mjs +24 -7
  103. package/runtime/scripts/hfs/rules/edition.mjs +9 -8
  104. package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
  105. package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
  106. package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
  107. package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
  108. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
  109. package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
  110. package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
  111. package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
  112. package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
  113. package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
  114. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
  115. package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
  116. package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
  117. package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
  118. package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
  119. package/runtime/scripts/hfs/rules/saga.mjs +8 -16
  120. package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
  121. package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
  122. package/runtime/scripts/hfs/rules/secrets.mjs +13 -0
  123. package/runtime/scripts/hfs/rules/services.mjs +4 -4
  124. package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
  125. package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
  126. package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
  127. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
  128. package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
  129. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +41 -1
  130. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
  131. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +1 -1
  132. package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
  133. package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
  134. package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
  135. package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
  136. package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
  137. package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
  138. package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
  139. package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
  140. package/runtime/scripts/hfs/sealed-members.mjs +101 -0
  141. package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
  142. package/runtime/scripts/hfs/secret-edit.mjs +170 -0
  143. package/runtime/scripts/hfs/secret-init.mjs +127 -19
  144. package/runtime/scripts/hfs/secret-sops.mjs +38 -6
  145. package/runtime/scripts/hfs/secret-store.mjs +56 -54
  146. package/runtime/scripts/hfs/secret.mjs +34 -5
  147. package/runtime/scripts/hfs/slot-imports.mjs +18 -5
  148. package/runtime/scripts/hfs/slots.mjs +1 -0
  149. package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
  150. package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
  151. package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
  152. package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
  153. package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
  154. package/runtime/scripts/lib/command-tokens.mjs +126 -0
  155. package/runtime/scripts/lib/compose-values.mjs +11 -0
  156. package/runtime/scripts/lib/deployment-findings.mjs +7 -0
  157. package/runtime/scripts/lib/json-clone.mjs +61 -0
  158. package/runtime/scripts/lib/list.mjs +4 -0
  159. package/runtime/scripts/lib/runtime-example.mjs +13 -0
  160. package/runtime/scripts/lib/secret-patterns.mjs +60 -5
  161. package/runtime/scripts/lib/sql-ast.mjs +32 -0
  162. package/runtime/scripts/lib/stack-services.mjs +1 -1
  163. package/runtime/scripts/lib/swarm-stack.mjs +5 -18
  164. package/runtime/scripts/lib/trim-end.mjs +8 -0
  165. package/runtime/scripts/lib/ts-analysis.mjs +5 -2
  166. package/runtime/scripts/lib/ts-ast.mjs +27 -2
  167. package/runtime/scripts/lib/workflow-events.mjs +8 -0
  168. package/scaffold/add-app.mjs +77 -59
  169. package/scaffold/add-cli-lite.mjs +14 -6
  170. package/scaffold/add-compose.mjs +79 -0
  171. package/scaffold/add-database-lite.mjs +14 -0
  172. package/scaffold/add-fe-overlay.mjs +122 -0
  173. package/scaffold/add-fe-page.mjs +269 -0
  174. package/scaffold/add-inbox-lite.mjs +74 -42
  175. package/scaffold/add-table.mjs +110 -87
  176. package/scaffold/add.mjs +159 -44
  177. package/scaffold/app.mjs +124 -39
  178. package/scaffold/edition-gate.mjs +3 -3
  179. package/scaffold/lite-exports.mjs +2 -1
  180. package/scaffold/service.mjs +2 -2
  181. package/src/main.mjs +229 -116
  182. package/stack/age-install.mjs +37 -0
  183. package/stack/backup.mjs +100 -0
  184. package/stack/build-args.mjs +42 -0
  185. package/stack/build.mjs +173 -0
  186. package/stack/cli.mjs +130 -0
  187. package/stack/config.mjs +57 -0
  188. package/stack/context.mjs +57 -0
  189. package/stack/converge.mjs +107 -0
  190. package/stack/deploy.mjs +301 -0
  191. package/stack/digest.mjs +16 -0
  192. package/stack/docker-transport.mjs +53 -0
  193. package/stack/edge-alias.mjs +118 -0
  194. package/stack/errors.mjs +21 -0
  195. package/stack/image-scan.mjs +152 -0
  196. package/stack/ingress-create.mjs +114 -0
  197. package/stack/ingress-publish.mjs +273 -0
  198. package/stack/ingress.mjs +706 -0
  199. package/stack/jobs.mjs +51 -0
  200. package/stack/live-restore.mjs +177 -0
  201. package/stack/prepare.mjs +266 -0
  202. package/stack/private-file.mjs +22 -0
  203. package/stack/provision.mjs +90 -0
  204. package/stack/prune.mjs +46 -0
  205. package/stack/release-store.mjs +87 -0
  206. package/stack/remote.mjs +205 -0
  207. package/stack/render.mjs +149 -0
  208. package/stack/retire.mjs +47 -0
  209. package/stack/sealed.mjs +149 -0
  210. package/stack/task.mjs +42 -0
  211. package/stack/verify.mjs +99 -0
  212. package/sync/ci-commands.mjs +13 -0
  213. package/sync/index.mjs +108 -52
  214. package/sync/stack-images.mjs +127 -0
  215. package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
  216. package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
  217. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
  218. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
  219. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
  220. package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
  221. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
  222. package/templates/app/codegen-graphql.mjs.partial +13 -2
  223. package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
  224. package/templates/app/codegen-prelude.mjs.partial +8 -0
  225. package/templates/app/gitignore +3 -0
  226. package/templates/app/gitignore-lite +3 -0
  227. package/templates/app/package-scripts/package.json +1 -0
  228. package/templates/app/package-scripts/supabase.json +7 -0
  229. package/templates/app/package-scripts-lite/package.json +1 -7
  230. package/templates/app/skeleton/.editorconfig +1 -0
  231. package/templates/app/skeleton/.gitattributes +7 -1
  232. package/templates/app/skeleton/.github/dependabot.yml +17 -0
  233. package/templates/app/skeleton/.nvmrc +1 -1
  234. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
  235. package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
  236. package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
  237. package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
  238. package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
  239. package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
  240. package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
  241. package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
  242. package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  243. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  244. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  245. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
  246. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  247. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
  248. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
  249. package/templates/app/skeleton/README.md +6 -6
  250. package/templates/app/skeleton/scripts/codegen.mjs +1 -2
  251. package/templates/app/skeleton-lite/.editorconfig +16 -0
  252. package/templates/app/skeleton-lite/.gitattributes +8 -0
  253. package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
  254. package/templates/app/skeleton-lite/.nvmrc +1 -0
  255. package/templates/app/skeleton-lite/.sops.yaml +3 -2
  256. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +178 -1
  257. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
  258. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
  259. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
  260. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
  261. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
  262. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
  263. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
  264. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
  265. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
  266. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
  267. package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
  268. package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
  269. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
  270. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  271. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  272. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  273. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
  274. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  275. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
  276. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
  277. package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
  278. package/templates/app/skeleton-lite/README.md +5 -3
  279. package/templates/app/skeleton-lite/supabase/config.toml +8 -1
  280. package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
  281. package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
  282. package/templates/be/image/worker/Dockerfile +1 -1
  283. package/templates/be/inbox/database-client.spec.ts.tpl +17 -18
  284. package/templates/be/inbox/database-client.ts.tpl +12 -11
  285. package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
  286. package/templates/be/inbox/event-index.ts.tpl +1 -0
  287. package/templates/be/inbox/events.json.tpl +10 -0
  288. package/templates/be/inbox/migration.sql.tpl +1 -0
  289. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
  290. package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
  291. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
  292. package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
  293. package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
  294. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
  295. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
  296. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
  297. package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
  298. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
  299. package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
  300. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
  301. package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
  302. package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
  303. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
  304. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
  305. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
  306. package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
  307. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
  308. package/templates/be/inbox/platform/migration.sql.tpl +48 -0
  309. package/templates/be/inbox/service.spec.ts.tpl +42 -9
  310. package/templates/be/inbox/service.ts.tpl +23 -8
  311. package/templates/be/inbox/webhook-door.spec.ts.tpl +1 -1
  312. package/templates/be/inbox/webhook-door.ts.tpl +2 -2
  313. package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
  314. package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
  315. package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
  316. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
  317. package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
  318. package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
  319. package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
  320. package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
  321. package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
  322. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
  323. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
  324. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
  325. package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
  326. package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
  327. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
  328. package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
  329. package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
  330. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
  331. package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
  332. package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
  333. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
  334. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
  335. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
  336. package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
  337. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
  338. package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
  339. package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
  340. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
  341. package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
  342. package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
  343. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
  344. package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
  345. package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
  346. package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
  347. package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
  348. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
  349. package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
  350. package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
  351. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
  352. package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
  353. package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
  354. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
  355. package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
  356. package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
  357. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
  358. package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
  359. package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
  360. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
  361. package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
  362. package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
  363. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
  364. package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
  365. package/templates/be/skeleton/apps/__app__/src/app.module.ts +11 -9
  366. package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
  367. package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
  368. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +2 -0
  369. package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
  370. package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
  371. package/templates/be/skeleton/apps/__app__/src/main.ts +15 -1
  372. package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
  373. package/templates/be/skeleton/apps/cli/src/cli.options.ts +6 -14
  374. package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
  375. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
  376. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
  377. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
  378. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
  379. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
  380. package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
  381. package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
  382. package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
  383. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
  384. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
  385. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
  386. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
  387. package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
  388. package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
  389. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
  390. package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
  391. package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
  392. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
  393. package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
  394. package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
  395. package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
  396. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
  397. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
  398. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
  399. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
  400. package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
  401. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
  402. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
  403. package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
  404. package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
  405. package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
  406. package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
  407. package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
  408. package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
  409. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
  410. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
  411. package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
  412. package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
  413. package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
  414. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
  415. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
  416. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
  417. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
  418. package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
  419. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
  420. package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
  421. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
  422. package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
  423. package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
  424. package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
  425. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
  426. package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
  427. package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
  428. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
  429. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
  430. package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
  431. package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
  432. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
  433. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
  434. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
  435. package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
  436. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
  437. package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
  438. package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
  439. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
  440. package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
  441. package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
  442. package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
  443. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
  444. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
  445. package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
  446. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +14 -18
  447. package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
  448. package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
  449. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
  450. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +2 -0
  451. package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
  452. package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
  453. package/templates/be/skeleton-lite/apps/api/src/main.ts +2 -3
  454. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
  455. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
  456. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
  457. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
  458. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
  459. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
  460. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
  461. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
  462. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
  463. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
  464. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
  465. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
  466. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
  467. package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
  468. package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
  469. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
  470. package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
  471. package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
  472. package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
  473. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
  474. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
  475. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
  476. package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
  477. package/templates/be/table/api/feature.module.ts.tpl +2 -1
  478. package/templates/be/table/api/feature.options.ts.tpl +2 -0
  479. package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
  480. package/templates/be/table/api/graphql.module.ts.tpl +3 -2
  481. package/templates/be/table/api/graphql.options.ts.tpl +2 -0
  482. package/templates/be/table/migration.sql.tpl +1 -0
  483. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
  484. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
  485. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
  486. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
  487. package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
  488. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
  489. package/templates/fe/auth/supabase-client.ts.tpl +34 -0
  490. package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
  491. package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
  492. package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
  493. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
  494. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
  495. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
  496. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
  497. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
  498. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
  499. package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
  500. package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
  501. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
  502. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
  503. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
  504. package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
  505. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
  506. package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
  507. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
  508. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
  509. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
  510. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
  511. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
  512. package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
  513. package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
  514. package/upgrade/check.mjs +3 -1
  515. package/upgrade/index.mjs +53 -40
package/src/main.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  // The @starci/hfs implementation behind `starci app`: one repository, `<app>/`, with one package.json, lockfile and node_modules
2
2
  // at its root and two sides, be/ and fe/, declared by the one hfs.json of kind app. Every verb runs at the app root.
3
- // starci app check [--cwd <dir>] [--json] [--fast] [--base <ref>]
3
+ // starci app check [--cwd <dir>] [--json] [--fast] [--base <ref>] [--wiring] [--coverage-from <lcov>]
4
4
  // every tracked path has a slot; required files exist; nothing forbidden or
5
5
  // tracked-that-must-be-ignored; pins match; every managed file equals its render
6
6
  // (sync/managed.mjs, the .gitignore block and sonar-project.properties included); no empty or ghost
@@ -33,13 +33,18 @@
33
33
  // a back-end `<name>.service.ts` and its `<name>.service.spec.ts` skeleton (scaffold/service.mjs; <dir> is
34
34
  // relative to be/): the spec is built
35
35
  // with Test.createTestingModule, one provider per constructor dependency (kit doubles from @starci/jest-preset),
36
- // one placeholder it per public method. Never overwrites a file. Both editions: a lite back end keeps full's unit specs (owner ruling 2026-10-12).
36
+ // one placeholder it per public method. Never overwrites a file. Both editions: a lite back end keeps full's unit specs (ruling:R212/case-1 (owner:lite-production-swarm), amended by ruling:R212/case-4).
37
37
  // starci app new image [--cwd <dir>] the Dockerfile of every declared app that has none, from the image canon (scaffold/image.mjs); never overwrites
38
38
  // starci app new spec <file>.service.ts [--cwd <dir>] the spec skeleton of an existing service, read from its constructor with the repository's TypeScript
39
+ // starci app stack prepare|doctor|connect|disconnect|build-args|render|deploy|converge|verify|rollback|prune|backup|jobs --env <env> [--execute]
40
+ // the Swarm deploy tooling of stack/ (dry run unless --execute; never prints a secret value; see docs/application-stacks.md).
39
41
  // starci app secret init | set <env> <slug> <KEY> [--generate <bytes>] | get <env> <slug> [KEY] --to-file [--reveal]
40
- // starci app secret materialize <env> | list <env>
42
+ // starci app secret unset <env> <slug> <KEY> [--execute] | rename <env> <slug> <OLD> <NEW> [--execute] | remove <env> <slug> [--execute] [--force-nonempty]
43
+ // starci app secret materialize <env> | list <env> | verify <env>
41
44
  // dotenv custody in .starcistacks, public age rule initialization, private KEY files outside Git;
42
45
  // values reach stdout only when get explicitly receives --reveal.
46
+ // starci app db baseline | restamp --map <restamp.yaml> | plan
47
+ // preview the database cutover or compare its state with the repo; baseline and restamp mutate only with --apply.
43
48
  // starci app add <api|webhook|realtime|saga|job|reactor|queue|projection> <name> [--event <event> --from <service> --service <Class>=<module>] [--owner <service> --failed <event> --done <event>] [--connection <name>] [--cwd <dir>]
44
49
  // exactly that kind's file tree, generated FROM the files: tree of its pattern topic (knowledge/patterns/be) with the
45
50
  // one template body of each entry (templates/be/patterns), plus the platform capabilities it needs when they are missing;
@@ -51,8 +56,12 @@ import { createRequire } from 'node:module';
51
56
  import path from 'node:path';
52
57
  import { fileURLToPath, pathToFileURL } from 'node:url';
53
58
  import { isMain } from '../runtime/scripts/lib/is-main.mjs';
54
- import { checkDatabase, checkRepository, explainPath, trackedFiles } from '../runtime/scripts/hfs/check.mjs';
59
+ import { checkDatabase, checkRepository, explainPath, trackedFiles, withCheckProofs } from '../runtime/scripts/hfs/check.mjs';
55
60
  import { secretMain } from '../runtime/scripts/hfs/secret.mjs';
61
+ import { dbBaselineMain } from '../runtime/scripts/hfs/db-baseline-verb.mjs';
62
+ import { dbRestampMain } from '../runtime/scripts/hfs/db-restamp-verb.mjs';
63
+ import { dbPlanMain } from '../runtime/scripts/hfs/db-plan-verb.mjs';
64
+ import { formatCoverageReport } from '../runtime/scripts/hfs/coverage-from.mjs';
56
65
  import { HFS_DECLARATION_FILE, HfsSlotsError, loadSlotManifest, readRepoDeclaration } from '../runtime/scripts/hfs/slots.mjs';
57
66
  import { formatFindings } from '../sync/format.mjs';
58
67
  import { main as syncMain } from '../sync/cli.mjs';
@@ -70,16 +79,29 @@ import { lintRepository, parseLintArgs, printLintText } from '../lint/run.mjs';
70
79
  import { writeReport } from '../report/sonar.mjs';
71
80
  import { checkEdition } from '../upgrade/check.mjs';
72
81
  import { UpgradeError, upgradeMain } from '../upgrade/index.mjs';
82
+ import { ACTIONS as STACK_ACTIONS, stackMain as deployStackMain } from '../stack/cli.mjs';
83
+ import { releaseMain } from '../release/index.mjs';
84
+ import { dbMigrateMain } from '../db/migrate.mjs';
85
+ import { dbPostCutoverMain } from '../db/post-cutover.mjs';
73
86
 
74
- export const VERBS = Object.freeze(['scaffold', 'add', 'lint', 'sync', 'check', 'upgrade', 'stack', 'explain', 'emit', 'new', 'secret', 'hygiene']);
87
+ export const VERBS = Object.freeze(['scaffold', 'add', 'lint', 'sync', 'check', 'upgrade', 'stack', 'release', 'explain', 'emit', 'new', 'secret', 'db', 'hygiene']);
88
+ const DATABASE_VERBS = new Map([['baseline', dbBaselineMain], ['restamp', dbRestampMain], ['plan', dbPlanMain], ['migrate', dbMigrateMain], ['post-cutover', dbPostCutoverMain]]);
75
89
 
76
- const USAGE = `starci app scaffold <name> [--into <dir>] [--cwd <dir>]
90
+ const USAGE = `starci app scaffold <name> [--into <dir>] [--edition full|lite] [--registry-visibility public|private] [--cwd <dir>]
77
91
  starci app add <api|webhook|realtime|saga|job|reactor|queue|projection|cli|table|app> <name> [options] [--cwd <dir>]
78
92
  starci app lint [--cwd <dir>] [--changed <file>...] [--workspace <dir>] [--fix] [--format text|json] [--sonar <file>]
79
93
  starci app sync (--check | --write) [--cwd <dir>]
80
- starci app check [--cwd <dir>] [--json] [--fast] [--base <ref>] [--edition full] [--db-types]
94
+ starci app check [--cwd <dir>] [--json] [--fast] [--base <ref>] [--edition full] [--db-types] [--wiring] [--coverage-from <lcov>]
95
+ starci app db baseline [--stamp <version>] [--local] [--apply] [--cwd <dir>]
96
+ starci app db restamp --map <restamp.yaml> [--local] [--apply] [--cwd <dir>]
97
+ starci app db plan [--local] [--json] [--cwd <dir>]
98
+ starci app db migrate [--execute] [--linked | --url-env <NAME>] [--json] [--cwd <dir>]
99
+ starci app db post-cutover [--execute] [--green-at <ISO> | --owner-confirms-old-app-gone] [--linked | --url-env <NAME>] [--json] [--cwd <dir>]
100
+ starci app release gate [--execute] [--source <ref>] [--tag-prefix <prefix>] [--json] [--cwd <dir>]
101
+ starci app release push [--execute] [--remote <name>] [--json] [--cwd <dir>]
81
102
  starci app upgrade --edition full [--plan] [--cwd <dir>]
82
103
  starci app stack <up|down|status> [--stack <dir>] [--services <list>] [--k3d] [--force] [--json] [--cwd <dir>]
104
+ starci app stack <prepare|doctor|connect|disconnect|build-args|render|deploy|converge|verify|rollback|prune|backup|jobs> --env <env> [--execute] [--json] [--cwd <dir>]
83
105
  starci app explain <path> [--cwd <dir>] [--json]
84
106
  starci app emit [--cwd <dir>]
85
107
  starci app new service <dir> <name> [--inject <binding>...] [--cwd <dir>]
@@ -88,24 +110,45 @@ starci app new image [--cwd <dir>]
88
110
  starci app secret init [--cwd <dir>]
89
111
  starci app secret set <env> <slug> <KEY> [--generate <bytes>] [--cwd <dir>]
90
112
  starci app secret get <env> <slug> [KEY] --to-file [--reveal] [--cwd <dir>]
113
+ starci app secret unset <env> <slug> <KEY> [--execute] [--cwd <dir>]
114
+ starci app secret rename <env> <slug> <OLD> <NEW> [--execute] [--cwd <dir>]
115
+ starci app secret remove <env> <slug> [--execute] [--force-nonempty] [--cwd <dir>]
91
116
  starci app secret materialize <env> [--cwd <dir>]
92
117
  starci app secret list <env> [--cwd <dir>]
118
+ starci app secret verify <env> [--cwd <dir>]
93
119
  starci app hygiene [--cwd <dir>]
94
120
  `;
95
121
  const PER_CODE_LIMIT = 25;
96
- const VALUE_FLAGS = new Set(['--base', '--inject', '--into', '--event', '--from', '--service', '--connection', '--owner', '--failed', '--done']);
122
+ const VALUE_FLAGS = new Set(['--base', '--inject', '--into', '--event', '--from', '--service', '--connection', '--owner', '--failed', '--done', '--coverage-from', '--registry-visibility']);
97
123
  /** Flags that may repeat: their values are collected in order. */
98
124
  const LIST_FLAGS = new Set(['--inject']);
99
- const BOOL_FLAGS = new Set(['--json', '--fast', '--fe', '--no-types', '--db-types']);
125
+ const BOOL_FLAGS = new Set(['--json', '--fast', '--fe', '--no-types', '--db-types', '--wiring']);
126
+
127
+ function coverageArgument(arg, args, opts) {
128
+ const inline = arg.startsWith('--coverage-from=');
129
+ const value = inline ? arg.slice('--coverage-from='.length) : args.next().value;
130
+ if (!value || value.startsWith('--')) throw new Error('--coverage-from needs a value');
131
+ if (opts['coverage-from'] !== undefined) throw new Error('--coverage-from may be given only once');
132
+ opts['coverage-from'] = value;
133
+ }
134
+
135
+ function valueArgument(arg, args, opts) {
136
+ const value = args.next().value;
137
+ if (value === undefined) throw new Error(`${arg} needs a value`);
138
+ const key = arg.slice(2);
139
+ opts[key] = LIST_FLAGS.has(arg) ? [...(opts[key] ?? []), value] : value;
140
+ }
100
141
 
101
142
  function parse(argv) {
102
143
  const opts = { positional: [] };
103
- for (let i = 0; i < argv.length; i += 1) {
104
- const arg = argv[i];
144
+ const args = argv.values();
145
+ for (const arg of args) {
146
+ if (arg === '--coverage-from' || arg.startsWith('--coverage-from=')) {
147
+ coverageArgument(arg, args, opts);
148
+ continue;
149
+ }
105
150
  if (VALUE_FLAGS.has(arg)) {
106
- if (argv[i + 1] === undefined) throw new Error(`${arg} needs a value`);
107
- opts[arg.slice(2)] = LIST_FLAGS.has(arg) ? [...(opts[arg.slice(2)] ?? []), argv[i + 1]] : argv[i + 1];
108
- i += 1;
151
+ valueArgument(arg, args, opts);
109
152
  }
110
153
  else if (BOOL_FLAGS.has(arg)) opts[arg.slice(2)] = true;
111
154
  else if (arg.startsWith('--')) throw new Error(`unknown flag ${arg}`);
@@ -114,34 +157,57 @@ function parse(argv) {
114
157
  return opts;
115
158
  }
116
159
 
117
- function printCheck(result, out) {
118
- const { counts } = result;
160
+ function machineDescription(machine) {
161
+ if (machine.status !== 'ran') return `skipped, ${machine.reason}`;
162
+ const owners = machine.paths ? ` (owners ${machine.paths.join(', ')})` : '';
163
+ return `ran over ${machine.files} source files${owners}`;
164
+ }
165
+
166
+ function contractDescription(contracts) {
167
+ if (contracts.status !== 'checked') return `skipped, ${contracts.reason}`;
168
+ const apps = contracts.apps.map((app) => `${app.app}/${app.artifact} ${app.status}`).join(', ') || 'no api app';
169
+ return `emitted and compared, ${apps}`;
170
+ }
171
+
172
+ function printCheckOverview(result, out) {
119
173
  out(`starci app check${result.fast ? ' --fast' : ''} ${result.repoRoot} (profile ${result.profile ?? 'unknown'}, manifest ${result.manifest}, ${result.tracked} tracked paths)\n`);
120
174
  if (result.fast) out(` ${result.fast.changed} path${result.fast.changed === 1 ? '' : 's'} changed since ${result.fast.base.slice(0, 12)}\n`);
121
- out(` architecture machine: ${result.machine.status === 'ran' ? `ran over ${result.machine.files} source files${result.machine.paths ? ` (owners ${result.machine.paths.join(', ')})` : ''}` : `skipped, ${result.machine.reason}`}\n`);
122
- if (result.contracts) out(` contract snapshots: ${result.contracts.status === 'checked' ? `emitted and compared, ${result.contracts.apps.map((a) => `${a.app}/${a.artifact} ${a.status}`).join(', ') || 'no api app'}` : `skipped, ${result.contracts.reason}`}
123
- `);
175
+ out(` architecture machine: ${machineDescription(result.machine)}\n`);
176
+ if (result.contracts) out(` contract snapshots: ${contractDescription(result.contracts)}\n`);
177
+ }
178
+
179
+ function printFindingGroup(code, list, out) {
180
+ const first = list[0];
181
+ out(`\n[${first.level.toUpperCase()}] ${code} x${list.length}: ${first.titleVi}\n ${first.whyVi}\n -> ${first.nextStepVi}\n`);
182
+ for (const finding of list.slice(0, PER_CODE_LIMIT)) out(` - ${finding.message}\n`);
183
+ if (list.length > PER_CODE_LIMIT) out(` ... and ${list.length - PER_CODE_LIMIT} more (use --json for all)\n`);
184
+ }
185
+
186
+ function printCheck(result, out) {
187
+ const { counts } = result;
188
+ printCheckOverview(result, out);
124
189
  const byCode = new Map();
125
190
  for (const f of result.findings) byCode.set(f.code, [...(byCode.get(f.code) ?? []), f]);
126
- for (const [code, list] of byCode) {
127
- const first = list[0];
128
- out(`\n[${first.level.toUpperCase()}] ${code} x${list.length}: ${first.titleVi}\n ${first.whyVi}\n -> ${first.nextStepVi}\n`);
129
- for (const f of list.slice(0, PER_CODE_LIMIT)) out(` - ${f.message}\n`);
130
- if (list.length > PER_CODE_LIMIT) out(` ... and ${list.length - PER_CODE_LIMIT} more (use --json for all)\n`);
131
- }
191
+ for (const [code, list] of byCode) printFindingGroup(code, list, out);
132
192
  out(`\n${counts.error} error finding${counts.error === 1 ? '' : 's'}, ${counts.warning} warning${counts.warning === 1 ? '' : 's'}, ${counts.info} report-only\n`);
133
193
  }
134
194
 
195
+ function printNoSlot(e, out) {
196
+ out(` no slot owns this path (${e.code}): ${e.titleVi}\n ${e.whyVi}\n`);
197
+ if (e.nearest) out(` nearest slot ${e.nearest.slot}, pattern ${e.nearest.pattern}; matched ${e.nearest.matchedPrefix || '.'}, then expected ${e.nearest.expectedNext ?? 'nothing'}\n`);
198
+ }
199
+
135
200
  function printExplain(e, out) {
136
201
  out(`${e.path}\n`);
137
202
  if (e.status === 'no-slot') {
138
- out(` no slot owns this path (${e.code}): ${e.titleVi}\n ${e.whyVi}\n`);
139
- if (e.nearest) out(` nearest slot ${e.nearest.slot}, pattern ${e.nearest.pattern}; matched ${e.nearest.matchedPrefix || '.'}, then expected ${e.nearest.expectedNext ?? 'nothing'}\n`);
203
+ printNoSlot(e, out);
140
204
  return;
141
205
  }
142
206
  if (e.status === 'ambiguous') { out(` owned equally by ${e.candidates.join(', ')} (${e.code}): ${e.titleVi}\n`); return; }
143
- out(` slot ${e.slot} (${e.pattern})\n presence ${e.presence}, ${e.tracking}\n tier ${e.tier}${e.owner ? ` owner ${e.owner.root} (${e.owner.slot})` : ''}\n`);
144
- out(` imports ${e.allowedImports ? `may import ${e.allowedImports.join(', ')}; ${e.importRule}` : (e.importRule ?? 'not applicable')}\n`);
207
+ const owner = e.owner ? ` owner ${e.owner.root} (${e.owner.slot})` : '';
208
+ const imports = e.allowedImports ? `may import ${e.allowedImports.join(', ')}; ${e.importRule}` : (e.importRule ?? 'not applicable');
209
+ out(` slot ${e.slot} (${e.pattern})\n presence ${e.presence}, ${e.tracking}\n tier ${e.tier}${owner}\n`);
210
+ out(` imports ${imports}\n`);
145
211
  out(` tests ${e.tests}: ${e.testsMeaning}\n`);
146
212
  if (e.requiredFiles.length) out(` requires ${e.requiredFiles.join(', ')}\n`);
147
213
  if (e.goesTo) out(` belongs at ${e.goesTo}\n`);
@@ -212,27 +278,29 @@ async function scaffoldPresets() {
212
278
  }
213
279
  }
214
280
 
281
+ function globalValueArgument(arg, args) {
282
+ const inline = arg.startsWith('--cwd=') || arg.startsWith('--edition=');
283
+ const flag = inline ? arg.slice(0, arg.indexOf('=')) : arg;
284
+ if (flag !== '--cwd' && flag !== '--edition') return null;
285
+ const value = inline ? arg.slice(arg.indexOf('=') + 1) : args.next().value;
286
+ if (!value || value.startsWith('--')) throw new Error(`${flag} needs a value`);
287
+ if (flag === '--edition' && !['full', 'lite'].includes(value)) throw new Error('--edition accepts full or lite');
288
+ return { flag, value };
289
+ }
290
+
215
291
  function globalOptions(argv, baseCwd) {
216
- const rest = [];
217
- let cwd = baseCwd;
218
- let quiet = false;
219
- let help = false;
220
- let edition;
221
- for (let i = 0; i < argv.length; i += 1) {
222
- const arg = argv[i];
223
- const inline = arg.startsWith('--cwd=') || arg.startsWith('--edition=') ? arg.slice(arg.indexOf('=') + 1) : null;
224
- const flag = inline === null ? arg : arg.slice(0, arg.indexOf('='));
225
- if (flag === '--cwd' || flag === '--edition') {
226
- const value = inline ?? argv[++i];
227
- if (!value || value.startsWith('--')) throw new Error(`${flag} needs a value`);
228
- if (flag === '--edition' && !['full', 'lite'].includes(value)) throw new Error('--edition accepts full or lite');
229
- if (flag === '--edition') edition = value;
230
- if (flag === '--cwd') cwd = path.resolve(baseCwd, value);
231
- } else if (flag === '--quiet') quiet = true;
232
- else if (flag === '--help') help = true;
233
- else rest.push(arg);
292
+ const options = { argv: [], cwd: baseCwd, quiet: false, help: false, edition: undefined };
293
+ const args = argv.values();
294
+ for (const arg of args) {
295
+ const valued = globalValueArgument(arg, args);
296
+ if (valued) {
297
+ if (valued.flag === '--cwd') options.cwd = path.resolve(baseCwd, valued.value);
298
+ else options.edition = valued.value;
299
+ } else if (arg === '--quiet') options.quiet = true;
300
+ else if (arg === '--help') options.help = true;
301
+ else options.argv.push(arg);
234
302
  }
235
- return { argv: rest, cwd, quiet, help, edition };
303
+ return options;
236
304
  }
237
305
 
238
306
  async function installedStackMain(cwd) {
@@ -247,10 +315,119 @@ async function installedStackMain(cwd) {
247
315
  return candidate;
248
316
  }
249
317
 
250
- /** `cwd`, `presets`, `prettier`, `lock`, `run` and `stackMain` are test seams (`run` is the Supabase types command runner); stdout and stderr never have to be process globals. */
318
+ function databaseMain(argv, io) {
319
+ const [verb, ...rest] = argv;
320
+ const dispatch = DATABASE_VERBS.get(verb);
321
+ if (!dispatch) throw new Error('starci app db takes baseline, restamp, plan, migrate or post-cutover');
322
+ return dispatch(rest, io);
323
+ }
324
+
325
+ function printCheckProofs(result, out) {
326
+ if (result.wiring) out(`boot wiring: ${result.wiring.status}, ${result.wiring.apps.length} app(s), ${result.wiring.findings.length} failure(s)\n`);
327
+ if (result.coverage) out(formatCoverageReport(result.coverage));
328
+ }
329
+
330
+ async function checkMain(opts, { cwd: repoRoot, edition, stdout: out, presets, prettier }) {
331
+ if (opts.positional.length) throw new Error('starci app check takes no path');
332
+ if (opts.base !== undefined && opts.fast !== true) throw new Error('--base names the ref --fast compares with; it needs --fast');
333
+ const checked = await checkEdition({ repoRoot, edition, normal: () => runCheck({ repoRoot, fast: opts.fast === true, base: opts.base, presets, prettier, dbTypes: opts['db-types'] === true }), presets, prettier });
334
+ const result = withCheckProofs(checked, { repoRoot, edition, wiring: opts.wiring === true, coverageFrom: opts['coverage-from'] });
335
+ if (opts.json) out(`${JSON.stringify(result, null, 2)}\n`);
336
+ else { printCheck(result, out); printCheckProofs(result, out); }
337
+ return result.ok ? 0 : 1;
338
+ }
339
+
340
+ function emitMain(opts, { cwd: repoRoot, stdout: out, run }) {
341
+ if (opts.positional.length) throw new Error('starci app emit takes no path');
342
+ const be = readRepoDeclaration(loadSlotManifest(), repoRoot).sides?.be;
343
+ if (!be) throw new Error('starci app emit runs at the app root (the folder of hfs.json)');
344
+ const { written, skipped, standIns } = emitContracts({ repoRoot: path.join(repoRoot, 'be'), declaration: be, run });
345
+ for (const file of written) out(`wrote be/${file}\n`);
346
+ const skipSummary = skipped.length ? `, ${skipped.join(', ')} serve no GraphQL` : '';
347
+ out(`starci app emit: ${written.length} written${skipSummary}\n`);
348
+ for (const [app, lines] of Object.entries(standIns)) {
349
+ out(`${app}: ${lines.length} dependenc${lines.length === 1 ? 'y' : 'ies'} of the resolvers could not load here and stood in (no GraphQL type is affected):\n`);
350
+ for (const line of lines) out(` ${line}\n`);
351
+ }
352
+ return 0;
353
+ }
354
+
355
+ function newMain(opts, { cwd: repoRoot, stdout: out }) {
356
+ const [kind, ...args] = opts.positional;
357
+ let written;
358
+ if (kind === 'service' && args.length === 2) written = newService({ repoRoot, dir: args[0], name: args[1], inject: opts.inject ?? [] });
359
+ else if (kind === 'spec' && args.length === 1 && opts.inject === undefined) written = newSpec({ repoRoot, file: args[0] });
360
+ else if (kind === 'image' && args.length === 0 && opts.inject === undefined) written = newImages({ repoRoot });
361
+ else throw new Error('starci app new takes `service <dir> <name> [--inject ...]`, `spec <file>.service.ts` or `image`');
362
+ for (const file of written) out(`created ${file}\n`);
363
+ return 0;
364
+ }
365
+
366
+ function addMain(opts, { cwd: repoRoot, stdout: out }) {
367
+ const [noun, name, ...extra] = opts.positional;
368
+ if (!noun || !name || extra.length) throw new Error('starci app add takes `<noun> <name>` and the options of the noun');
369
+ const { created, registered } = addKind({ repoRoot, noun, name, options: { event: opts.event, from: opts.from, service: opts.service, connection: opts.connection, owner: opts.owner, failed: opts.failed, done: opts.done, fe: opts.fe, noTypes: opts['no-types'] } });
370
+ for (const file of created) out(`created ${file}\n`);
371
+ const kinds = registered.kinds.length ? `; kinds ${registered.kinds.join(', ')}` : '';
372
+ out(`starci app add ${noun}: registered patterns ${registered.patterns.join(', ')}${kinds}\n`);
373
+ return 0;
374
+ }
375
+
376
+ async function scaffoldMain(opts, { cwd, edition = 'full', presets, lock, stdout: out }) {
377
+ const [name, ...extra] = opts.positional;
378
+ if (!name || extra.length) throw new Error('starci app scaffold takes `<name> [--into <dir>] [--edition full|lite] [--registry-visibility public|private]`');
379
+ const { root: created, files } = scaffoldApp({ name, into: path.resolve(cwd, opts.into ?? '.'), edition, presets: presets ?? await scaffoldPresets(), ...(opts['registry-visibility'] === undefined ? {} : { registryVisibility: opts['registry-visibility'] }), ...(lock ? { lock } : {}) });
380
+ out(`starci app scaffold: created ${created} (${files.length} files); next: npm ci, then starci app lint\n`);
381
+ return 0;
382
+ }
383
+
384
+ function explainMain(opts, { cwd: repoRoot, stdout: out }) {
385
+ if (opts.positional.length !== 1) throw new Error('starci app explain takes exactly one path');
386
+ const explained = explainPath({ repoRoot, input: opts.positional[0] });
387
+ if (opts.json) out(`${JSON.stringify(explained, null, 2)}\n`); else printExplain(explained, out);
388
+ return explained.status === 'no-slot' || explained.status === 'ambiguous' ? 1 : 0;
389
+ }
390
+
391
+ function upgradeAppMain(argv, { cwd, edition, stdout, presets }) {
392
+ return upgradeMain(['--repo', cwd, ...(edition ? ['--edition', edition] : []), ...argv], { stdout, presets });
393
+ }
394
+
395
+ /** The edition `hfs.json` declares, or undefined when it cannot be read (the test-world verbs then decide on their own). */
396
+ function declaredEdition(cwd) {
397
+ try { return JSON.parse(fs.readFileSync(path.join(cwd, HFS_DECLARATION_FILE), 'utf8')).edition; } catch { return undefined; }
398
+ }
399
+
400
+ /**
401
+ * `starci app stack`: the deploy actions (prepare, doctor, render, deploy, converge, verify, rollback, prune, backup, jobs, ...) are the Swarm
402
+ * tooling of stack/cli.mjs; up, down and status are the full edition's test-world infrastructure.
403
+ */
404
+ async function stackAppMain(argv, { cwd, edition, stackMain, stdout: out, stderr: err, env, sops, stackIo }) {
405
+ if (STACK_ACTIONS.includes(argv[0])) return deployStackMain(argv, { ...stackIo, cwd, stdout: out, stderr: err, env, ...(sops ? { sops } : {}) });
406
+ if ((edition ?? declaredEdition(cwd)) === 'lite') throw new Error('starci app stack up|down|status manage the local test world of the full edition; a lite app starts its local infrastructure with the Supabase CLI');
407
+ const run = stackMain ?? await installedStackMain(cwd);
408
+ return run(argv, { cwd, out, err });
409
+ }
410
+
411
+ const DIRECT_VERBS = new Map([['lint', lintMain], ['secret', secretMain], ['db', databaseMain], ['upgrade', upgradeAppMain], ['stack', stackAppMain], ['release', releaseMain]]);
412
+ const PARSED_VERBS = new Map([['check', checkMain], ['emit', emitMain], ['new', newMain], ['add', addMain], ['scaffold', scaffoldMain], ['explain', explainMain]]);
413
+
414
+ function dispatchMain(verb, argv, io) {
415
+ if (verb === 'sync' || verb === 'hygiene') return syncMain([verb, ...argv], io);
416
+ const direct = DIRECT_VERBS.get(verb);
417
+ if (direct) return direct(argv, io);
418
+ return PARSED_VERBS.get(verb)(parse(argv), io);
419
+ }
420
+
421
+ function errorMessage(error) {
422
+ if (error instanceof EditionRefusal || error instanceof UpgradeError || error instanceof HfsSlotsError || error instanceof SyncError) return `${error.message}\n`;
423
+ if (error instanceof ScaffoldError) return `${error.code}: ${error.message}\n`;
424
+ return `starci app: ${error.message}\n${USAGE}`;
425
+ }
426
+
427
+ /** `run` injects the Supabase command runner and `stackIo` the spawn, fetch, sleep and clock seams of the Swarm tooling; IO and tool seams keep every verb independently testable. */
251
428
  export async function main(argv, {
252
429
  cwd: baseCwd = process.cwd(), stdout = (s) => process.stdout.write(s), stderr = (s) => process.stderr.write(s), presets, prettier, lock, run, stackMain,
253
- env = process.env, stdin, sops, tools, platform,
430
+ env = process.env, stdin, sops, tools, platform, stackIo,
254
431
  } = {}) {
255
432
  try {
256
433
  const global = globalOptions(argv, path.resolve(baseCwd));
@@ -258,73 +435,9 @@ export async function main(argv, {
258
435
  const out = global.quiet ? () => {} : stdout;
259
436
  if (global.help) { out(USAGE); return 0; }
260
437
  if (!VERBS.includes(verb)) { stderr(USAGE); return 2; }
261
- if (verb === 'sync' || verb === 'hygiene') return await syncMain([verb, ...rest], { cwd: global.cwd, stdout: out });
262
- if (verb === 'lint') return await lintMain(rest, { cwd: global.cwd, stdout: out, presets, prettier });
263
- if (verb === 'secret') return secretMain(rest, { stdout: out, stderr, cwd: global.cwd, env, stdin, sops, tools, platform });
264
- if (verb === 'upgrade') return await upgradeMain(['--repo', global.cwd, ...(global.edition ? ['--edition', global.edition] : []), ...rest], { stdout: out, presets });
265
- if (verb === 'stack') {
266
- const run = stackMain ?? await installedStackMain(global.cwd);
267
- return await run(rest, { cwd: global.cwd, out, err: stderr });
268
- }
269
- const opts = parse(rest);
270
- const repoRoot = global.cwd;
271
- if (verb === 'check') {
272
- if (opts.positional.length) throw new Error('starci app check takes no path');
273
- if (opts.base !== undefined && opts.fast !== true) throw new Error('--base names the ref --fast compares with; it needs --fast');
274
- const result = await checkEdition({ repoRoot, edition: global.edition, normal: () => runCheck({ repoRoot, fast: opts.fast === true, base: opts.base, presets, prettier, dbTypes: opts['db-types'] === true }), presets, prettier });
275
- if (opts.json) out(`${JSON.stringify(result, null, 2)}\n`); else printCheck(result, out);
276
- return result.ok ? 0 : 1;
277
- }
278
- if (verb === 'emit') {
279
- if (opts.positional.length) throw new Error('starci app emit takes no path');
280
- const be = readRepoDeclaration(loadSlotManifest(), repoRoot).sides?.be;
281
- if (!be) throw new Error('starci app emit runs at the app root (the folder of hfs.json)');
282
- const { written, skipped, standIns } = emitContracts({ repoRoot: path.join(repoRoot, 'be'), declaration: be, run });
283
- for (const file of written) out(`wrote be/${file}
284
- `);
285
- out(`starci app emit: ${written.length} written${skipped.length ? `, ${skipped.join(', ')} serve no GraphQL` : ''}
286
- `);
287
- for (const [app, lines] of Object.entries(standIns)) {
288
- out(`${app}: ${lines.length} dependenc${lines.length === 1 ? 'y' : 'ies'} of the resolvers could not load here and stood in (no GraphQL type is affected):\n`);
289
- for (const line of lines) out(` ${line}\n`);
290
- }
291
- return 0;
292
- }
293
- if (verb === 'new') {
294
- const [kind, ...args] = opts.positional;
295
- let written;
296
- if (kind === 'service' && args.length === 2) written = newService({ repoRoot, dir: args[0], name: args[1], inject: opts.inject ?? [] });
297
- else if (kind === 'spec' && args.length === 1 && opts.inject === undefined) written = newSpec({ repoRoot, file: args[0] });
298
- else if (kind === 'image' && args.length === 0 && opts.inject === undefined) written = newImages({ repoRoot });
299
- else throw new Error('starci app new takes `service <dir> <name> [--inject ...]`, `spec <file>.service.ts` or `image`');
300
- for (const file of written) out(`created ${file}
301
- `);
302
- return 0;
303
- }
304
- if (verb === 'add') {
305
- const [noun, name, ...extra] = opts.positional;
306
- if (!noun || !name || extra.length) throw new Error('starci app add takes `<noun> <name>` and the options of the noun');
307
- const { created, registered } = addKind({ repoRoot, noun, name, options: { event: opts.event, from: opts.from, service: opts.service, connection: opts.connection, owner: opts.owner, failed: opts.failed, done: opts.done, fe: opts.fe, noTypes: opts['no-types'] } });
308
- for (const file of created) out(`created ${file}
309
- `);
310
- out(`starci app add ${noun}: registered patterns ${registered.patterns.join(', ')}${registered.kinds.length ? `; kinds ${registered.kinds.join(', ')}` : ''}
311
- `);
312
- return 0;
313
- }
314
- if (verb === 'scaffold') {
315
- const [name, ...extra] = opts.positional;
316
- if (!name || extra.length) throw new Error('starci app scaffold takes `<name> [--into <dir>] [--edition full|lite]`');
317
- const edition = global.edition ?? 'full';
318
- const { root: created, files } = scaffoldApp({ name, into: path.resolve(global.cwd, opts.into ?? '.'), edition, presets: presets ?? await scaffoldPresets(), ...(lock ? { lock } : {}) });
319
- out(`starci app scaffold: created ${created} (${files.length} files); next: npm ci, then starci app lint\n`);
320
- return 0;
321
- }
322
- if (opts.positional.length !== 1) throw new Error('starci app explain takes exactly one path');
323
- const explained = explainPath({ repoRoot, input: opts.positional[0] });
324
- if (opts.json) out(`${JSON.stringify(explained, null, 2)}\n`); else printExplain(explained, out);
325
- return explained.status === 'no-slot' || explained.status === 'ambiguous' ? 1 : 0;
438
+ return await dispatchMain(verb, rest, { cwd: global.cwd, edition: global.edition, stdout: out, stderr, presets, prettier, lock, run, stackMain, env, stdin, sops, tools, platform, stackIo });
326
439
  } catch (error) {
327
- stderr(error instanceof EditionRefusal || error instanceof UpgradeError ? `${error.message}\n` : error instanceof HfsSlotsError || error instanceof SyncError ? `${error.message}\n` : error instanceof ScaffoldError ? `${error.code}: ${error.message}\n` : `starci app: ${error.message}\n${USAGE}`);
440
+ stderr(errorMessage(error));
328
441
  return 2;
329
442
  }
330
443
  }
@@ -0,0 +1,37 @@
1
+ // age-install.mjs - installs `age` (the backup encryptor) on the manager over passwordless sudo: the host's own package manager first
2
+ // (dnf, yum, apk or apt-get, detected, never assumed), then the official static binary with its sha256 verified before anything is installed.
3
+ import { firstLine } from './context.mjs';
4
+
5
+ const AGE_VERSION = 'v1.2.1';
6
+ const AGE_URL = `https://github.com/FiloSottile/age/releases/download/${AGE_VERSION}/age-${AGE_VERSION}-linux-amd64.tar.gz`;
7
+ /** sha256 of the official linux/amd64 archive of AGE_VERSION. */
8
+ export const AGE_SHA256 = '7df45a6cc87d4da11cc03a539a7470c15b1041ab2b396af088fe9990f7c79d50';
9
+ const INSTALL = { dnf: 'sudo -n dnf install -y age', yum: 'sudo -n yum install -y age', apk: 'sudo -n apk add age', 'apt-get': 'sudo -n apt-get install -y age' };
10
+
11
+ const STATIC_INSTALL = [
12
+ 'set -e',
13
+ 'tmp="$(mktemp -d)"',
14
+ 'trap \'rm -rf "$tmp"\' EXIT',
15
+ `curl -fsSL -o "$tmp/age.tgz" ${AGE_URL}`,
16
+ `echo "${AGE_SHA256} $tmp/age.tgz" | sha256sum -c -`,
17
+ 'tar -xzf "$tmp/age.tgz" -C "$tmp"',
18
+ 'sudo -n install -m 0755 "$tmp/age/age" "$tmp/age/age-keygen" /usr/local/bin/',
19
+ ].join('; ');
20
+
21
+ const packageManager = (run) => run('for m in dnf yum apk apt-get; do command -v $m >/dev/null 2>&1 && echo $m && break; done').stdout.trim();
22
+ const present = (run) => run('command -v age').status === 0;
23
+
24
+ /** Installs age when it is missing; reports through `report`. */
25
+ export function installAge(run, report) {
26
+ if (present(run)) return;
27
+ const manager = packageManager(run);
28
+ let failure = 'no package manager (dnf, yum, apk, apt-get) found';
29
+ if (Object.hasOwn(INSTALL, manager)) {
30
+ const packaged = run(INSTALL[manager]);
31
+ if (packaged.status === 0 && present(run)) { report.note(`installed age with ${manager}`); return; }
32
+ failure = `${manager} could not install age: ${firstLine(packaged.stderr)}`;
33
+ }
34
+ const fetched = run(STATIC_INSTALL);
35
+ if (fetched.status === 0 && present(run)) report.note(`installed the official static age ${AGE_VERSION} (sha256 verified) into /usr/local/bin; ${failure}`);
36
+ else report.bad(`cannot install age (${failure}); the static install failed too: ${firstLine(fetched.stderr)} (needs passwordless sudo once, or install age by hand)`);
37
+ }
@@ -0,0 +1,100 @@
1
+ // backup.mjs - `starci app stack backup`: age-encrypted tarballs of the declared volumes, produced ON the manager.
2
+ // Per volume ONE remote command streams `tar` straight through `age` into <name>-<stamp>.tgz.age.partial and renames it to its final name only
3
+ // when the whole pipeline succeeded, so no plaintext file ever exists and a failed dump never leaves a truncated backup that looks valid.
4
+ // The age recipient is the public recipient of the app's .sops.yaml (the custody's own recipient). --swarm-state also archives
5
+ // /var/lib/docker/swarm (orchestration state; NOT a database backup). Backups older than keepDays are deleted after a fully successful run.
6
+ // The files are NOT moved off the machine: copying them and proving a restore on an isolated host is the owner's job.
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { firstLine } from './context.mjs';
10
+ import { failure, refusal } from './errors.mjs';
11
+ import { shellQuote, withRunner } from './remote.mjs';
12
+ import { parseYaml } from '../runtime/engine/yaml.mjs';
13
+
14
+ const RECIPIENT = /age1[0-9a-z]{50,}/u;
15
+
16
+ /** The first age recipient of the creation_rules of <repoRoot>/.sops.yaml. */
17
+ function readRecipient(repoRoot) {
18
+ let rules;
19
+ try { rules = parseYaml(fs.readFileSync(path.join(repoRoot, '.sops.yaml'), 'utf8'))?.creation_rules; } catch { rules = null; }
20
+ const found = RECIPIENT.exec(JSON.stringify(rules ?? null));
21
+ if (!found) throw refusal('STACK_BACKUP_NO_RECIPIENT', 'no age recipient found in the creation_rules of .sops.yaml');
22
+ return found[0];
23
+ }
24
+
25
+ /** YYYYMMDD-HHMMSS in UTC. */
26
+ const stampOf = (epochMs) => new Date(epochMs).toISOString().slice(0, 19).replaceAll('-', '').replaceAll(':', '').replace('T', '-');
27
+
28
+ function archiveJob(name, producer, { dir, stamp, recipient }) {
29
+ const final = `${dir}/${name}-${stamp}.tgz.age`;
30
+ const partial = `${final}.partial`;
31
+ const script = [
32
+ 'set -o pipefail', 'umask 077', `mkdir -p ${shellQuote(dir)}`,
33
+ `${producer} | age -r ${shellQuote(recipient)} -o ${shellQuote(partial)} && mv ${shellQuote(partial)} ${shellQuote(final)} || { rm -f ${shellQuote(partial)}; exit 1; }`,
34
+ ].join('; ');
35
+ return { name: `${name}-${stamp}.tgz.age`, command: `bash -c ${shellQuote(script)}` };
36
+ }
37
+
38
+ function planJobs(config, { swarmState, stamp, recipient }) {
39
+ const dir = config.backup.dir ?? `/var/backups/${config.stack}`;
40
+ const shared = { dir, stamp, recipient };
41
+ const jobs = config.backup.volumes.map((entry) => {
42
+ const mount = shellQuote(`${entry.volume}:/src:ro`);
43
+ return archiveJob(entry.name, `docker run --rm -v ${mount} ${shellQuote(config.backup.image)} tar czf - -C /src .`, shared);
44
+ });
45
+ if (swarmState) jobs.push(archiveJob('swarm-state', 'sudo tar czf - -C /var/lib/docker swarm', shared));
46
+ return { dir, jobs };
47
+ }
48
+
49
+ const pruneCommand = (dir, keepDays) => String.raw`find ${shellQuote(dir)} -type f \( -name '*.age' -o -name '*.age.partial' \) -mtime +${Number(keepDays)} -delete`;
50
+
51
+ function describePlan(config, plan, recipient, swarmState) {
52
+ const names = config.backup.volumes.map((entry) => entry.name);
53
+ return [
54
+ `volumes: ${names.join(', ')}${swarmState ? ' and the swarm state' : ''}`,
55
+ `target on the manager: ${plan.dir} (keep ${config.backup.keepDays} days)`,
56
+ `age recipient: ${recipient.slice(0, 12)}... (public recipient of .sops.yaml)`,
57
+ `files: ${plan.jobs.map((job) => job.name).join(', ')}`,
58
+ ];
59
+ }
60
+
61
+ const REMINDERS = [
62
+ 'the files are encrypted but still on the manager: copy them to storage in another failure domain',
63
+ 'a restore proof on an isolated host (age -d with the owner identity) is the job of the owner',
64
+ ];
65
+
66
+ function runArchives(run, plan) {
67
+ const lines = [];
68
+ const files = [];
69
+ for (const job of plan.jobs) {
70
+ const result = run(job.command);
71
+ if (result.status === 0) {
72
+ files.push(job.name);
73
+ lines.push(`ok ${job.name}`);
74
+ } else lines.push(`FAIL ${job.name}: ${firstLine(result.stderr)}`);
75
+ }
76
+ return { lines, files };
77
+ }
78
+
79
+ /** backup: the plan without execute; with execute the encrypted archives, then the retention sweep. */
80
+ export async function backupVolumes(ctx, config, { swarmState = false, execute = false, sshCmd = null } = {}, deps = {}) {
81
+ if (!config.backup) throw refusal('STACK_BACKUP_UNDECLARED', 'the environment declares no deploy.backup block');
82
+ const recipient = readRecipient(config.repoRoot);
83
+ const plan = planJobs(config, { swarmState, stamp: stampOf(ctx.now()), recipient });
84
+ const summary = describePlan(config, plan, recipient, swarmState);
85
+ if (!execute) return { ok: true, lines: ['dry run: nothing was backed up', ...summary], data: { files: [], planned: plan.jobs.map((job) => job.name) } };
86
+ return withRunner(ctx, config, { sshCmd }, deps, (run, notes) => {
87
+ if (run('command -v age >/dev/null 2>&1').status !== 0) throw refusal('STACK_BACKUP_NO_AGE', 'age is not installed on the manager (apt install age)');
88
+ const done = runArchives(run, plan);
89
+ const lines = [...notes, ...summary, ...done.lines];
90
+ const complete = done.files.length === plan.jobs.length;
91
+ if (complete) {
92
+ const pruned = run(pruneCommand(plan.dir, config.backup.keepDays));
93
+ if (pruned.status !== 0) throw failure('STACK_BACKUP_PRUNE', `deleting backups older than ${config.backup.keepDays} days failed: ${firstLine(pruned.stderr)}`);
94
+ lines.push(`ok removed backups older than ${config.backup.keepDays} days`);
95
+ } else lines.push('old backups were kept because this run did not finish');
96
+ lines.push(...REMINDERS);
97
+ if (swarmState) lines.push('the swarm state archive is orchestration state, not a database backup');
98
+ return { ok: complete, lines, data: { files: done.files } };
99
+ });
100
+ }
@@ -0,0 +1,42 @@
1
+ // build-args.mjs - `starci app stack build-args --app <app>`: the public build values of one image (literal values plus the declared keys
2
+ // of a sealed member). They are public by design (the browser receives them), so they are printed as KEY=VALUE; `data` carries key names only.
3
+ // In GitHub Actions each sealed value is masked first and the lines are appended to $GITHUB_OUTPUT as a multi-line `args` output.
4
+ import fs from 'node:fs';
5
+ import { refusal } from './errors.mjs';
6
+ import { openMember } from './sealed.mjs';
7
+
8
+ const OUTPUT_DELIMITER = 'BUILD_ARGS_OUTPUT_END';
9
+ const ONE_LINE = /^[^\r\n]+$/u;
10
+
11
+ function imageOf(config, app) {
12
+ const found = Object.entries(config.images).find(([, entry]) => entry.app === app);
13
+ if (!found) throw refusal('STACK_APP_UNKNOWN', `no image of the deploy block builds the app ${app}`);
14
+ return { service: found[0], buildArgs: found[1].buildArgs };
15
+ }
16
+
17
+ function checkedValue(key, value) {
18
+ if (typeof value !== 'string' || !ONE_LINE.test(value)) throw refusal('STACK_BUILD_ARG_INVALID', `${key} must be present and one line`);
19
+ return value;
20
+ }
21
+
22
+ function sealedValues(ctx, config, sealed) {
23
+ if (sealed === null) return {};
24
+ const map = openMember(ctx, config, sealed.member);
25
+ return Object.fromEntries(sealed.keys.map((key) => [key, checkedValue(key, map[key])]));
26
+ }
27
+
28
+ function publish(ctx, sealed, lines) {
29
+ if (ctx.env.GITHUB_ACTIONS !== 'true') return [];
30
+ if (ctx.env.GITHUB_OUTPUT) fs.appendFileSync(ctx.env.GITHUB_OUTPUT, `args<<${OUTPUT_DELIMITER}\n${lines.join('\n')}\n${OUTPUT_DELIMITER}\n`);
31
+ return Object.values(sealed).map((value) => `::add-mask::${value}`);
32
+ }
33
+
34
+ /** The build values of the image whose `app` is `options.app`. */
35
+ export async function buildArgs(ctx, config, options) {
36
+ const { service, buildArgs: declared } = imageOf(config, options.app);
37
+ const literal = Object.fromEntries(Object.entries(declared.values).map(([key, value]) => [key, checkedValue(key, value)]));
38
+ const sealed = sealedValues(ctx, config, declared.sealed);
39
+ const pairs = Object.entries({ ...literal, ...sealed }).map(([key, value]) => `${key}=${value}`);
40
+ const masks = publish(ctx, sealed, pairs);
41
+ return { ok: true, lines: [...masks, ...pairs], data: { app: options.app, service, keys: Object.keys({ ...literal, ...sealed }) } };
42
+ }