@starci/hfs 5.1.0 → 5.3.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 (365) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +11 -2
  3. package/cache/hfs-family.d.mts +2 -3
  4. package/cache/hfs-family.mjs +4 -9
  5. package/cache/secret-records.d.mts +7 -0
  6. package/cache/secret-records.mjs +12 -0
  7. package/cache/session.mjs +4 -2
  8. package/codemod/account-doors.d.mts +36 -0
  9. package/codemod/account-doors.mjs +128 -0
  10. package/codemod/main.mjs +31 -1
  11. package/codemod/module-definition.d.mts +52 -0
  12. package/codemod/module-definition.mjs +242 -0
  13. package/codemod/quality-binding.d.mts +33 -0
  14. package/codemod/quality-binding.mjs +38 -0
  15. package/codemod/secrets-cache.d.mts +35 -0
  16. package/codemod/secrets-cache.mjs +70 -0
  17. package/codemod/sops-rules.d.mts +34 -0
  18. package/codemod/sops-rules.mjs +69 -0
  19. package/codemod/theme-tokens.mjs +48 -2
  20. package/db/postgres.mjs +9 -1
  21. package/lint/run.d.mts +6 -0
  22. package/lint/run.mjs +63 -10
  23. package/package.json +1 -1
  24. package/runtime/knowledge/hfs/canon-pins.yaml +65 -11
  25. package/runtime/knowledge/hfs/fe-stack.yaml +9 -9
  26. package/runtime/knowledge/hfs/fork-images.yaml +7 -0
  27. package/runtime/knowledge/hfs/rules.yaml +67 -15
  28. package/runtime/knowledge/hfs/slots.yaml +174 -30
  29. package/runtime/modules/kernel/failure-codes.yaml +67 -4
  30. package/runtime/modules/schemas/application-stacks.schema.yaml +6 -0
  31. package/runtime/scripts/hfs/architecture/config.d.mts +4 -0
  32. package/runtime/scripts/hfs/architecture/config.mjs +25 -1
  33. package/runtime/scripts/hfs/architecture/identity.mjs +18 -83
  34. package/runtime/scripts/hfs/architecture/module-per-transport.d.mts +1 -1
  35. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +6 -1
  36. package/runtime/scripts/hfs/architecture/root-packages.d.mts +26 -0
  37. package/runtime/scripts/hfs/architecture/root-packages.mjs +78 -0
  38. package/runtime/scripts/hfs/architecture/transport-owner.d.mts +1 -2
  39. package/runtime/scripts/hfs/architecture/transport-owner.mjs +3 -6
  40. package/runtime/scripts/hfs/architecture/type-context.d.mts +4 -1
  41. package/runtime/scripts/hfs/architecture/type-context.mjs +30 -0
  42. package/runtime/scripts/hfs/architecture/typescript.d.mts +7 -1
  43. package/runtime/scripts/hfs/architecture/typescript.mjs +21 -1
  44. package/runtime/scripts/hfs/explicit-surface-rule.mjs +1 -1
  45. package/runtime/scripts/hfs/manifest-shape.d.mts +1 -0
  46. package/runtime/scripts/hfs/manifest-shape.mjs +3 -1
  47. package/runtime/scripts/hfs/rules/a15-findings.mjs +5 -0
  48. package/runtime/scripts/hfs/rules/database-access.mjs +39 -1
  49. package/runtime/scripts/hfs/rules/monorepo.d.mts +4 -0
  50. package/runtime/scripts/hfs/rules/monorepo.mjs +10 -5
  51. package/runtime/scripts/hfs/rules/secrets.mjs +11 -0
  52. package/runtime/scripts/hfs/rules/stacks-local.d.mts +7 -0
  53. package/runtime/scripts/hfs/rules/stacks-local.mjs +71 -0
  54. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +6 -1
  55. package/runtime/scripts/hfs/rules/stacks-service-images.d.mts +12 -0
  56. package/runtime/scripts/hfs/rules/stacks-service-images.mjs +120 -0
  57. package/runtime/scripts/hfs/secret-dev-init.d.mts +11 -0
  58. package/runtime/scripts/hfs/secret-dev-init.mjs +79 -0
  59. package/runtime/scripts/hfs/secret-errors.d.mts +3 -0
  60. package/runtime/scripts/hfs/secret-errors.mjs +8 -0
  61. package/runtime/scripts/hfs/secret-init.d.mts +6 -0
  62. package/runtime/scripts/hfs/secret-init.mjs +23 -0
  63. package/runtime/scripts/hfs/secret-sops.mjs +9 -1
  64. package/runtime/scripts/hfs/secret-standard.d.mts +20 -0
  65. package/runtime/scripts/hfs/secret-standard.mjs +106 -0
  66. package/runtime/scripts/hfs/secret.mjs +15 -8
  67. package/runtime/scripts/hfs/slots.mjs +1 -1
  68. package/runtime/scripts/hfs/stack-local-config.d.mts +101 -0
  69. package/runtime/scripts/hfs/stack-local-config.mjs +320 -0
  70. package/runtime/scripts/hfs/stacks-gate.mjs +1 -1
  71. package/runtime/scripts/hfs/view.d.mts +23 -0
  72. package/runtime/scripts/hfs/view.mjs +68 -0
  73. package/runtime/scripts/lib/declaration-surface.d.mts +4 -0
  74. package/runtime/scripts/lib/declaration-surface.mjs +55 -1
  75. package/runtime/scripts/lib/secret-records.d.mts +13 -0
  76. package/runtime/scripts/lib/secret-records.mjs +83 -0
  77. package/runtime/scripts/lib/swarm-stack.d.mts +2 -1
  78. package/runtime/scripts/lib/swarm-stack.mjs +2 -2
  79. package/scaffold/add-inbox-lite.mjs +2 -2
  80. package/scaffold/add-module.d.mts +16 -0
  81. package/scaffold/add-module.mjs +232 -0
  82. package/scaffold/add-queue-lite.mjs +50 -3
  83. package/scaffold/add.mjs +5 -0
  84. package/scaffold/app.d.mts +2 -2
  85. package/scaffold/app.mjs +16 -18
  86. package/scaffold/lite-exports.mjs +1 -15
  87. package/scaffold/module-registry.d.mts +19 -0
  88. package/scaffold/module-registry.mjs +84 -0
  89. package/scripts/sync-bundles.d.mts +8 -0
  90. package/src/main.mjs +23 -2
  91. package/stack/cli.mjs +6 -1
  92. package/stack/context.d.mts +15 -0
  93. package/stack/context.mjs +1 -0
  94. package/stack/images/openclaw-runtime/Dockerfile +18 -0
  95. package/stack/images/openclaw-runtime/entrypoint.sh +23 -0
  96. package/stack/images/openclaw-runtime/openclaw-init.mjs +101 -0
  97. package/stack/images/openclaw-runtime/openclaw-relay.mjs +25 -0
  98. package/stack/local-cli.d.mts +25 -0
  99. package/stack/local-cli.mjs +89 -0
  100. package/stack/local-config.d.mts +17 -0
  101. package/stack/local-config.mjs +37 -0
  102. package/stack/local-custody.d.mts +24 -0
  103. package/stack/local-custody.mjs +109 -0
  104. package/stack/local-doctor.d.mts +6 -0
  105. package/stack/local-doctor.mjs +49 -0
  106. package/stack/local-down.d.mts +8 -0
  107. package/stack/local-down.mjs +20 -0
  108. package/stack/local-host.d.mts +42 -0
  109. package/stack/local-host.mjs +163 -0
  110. package/stack/local-logs.d.mts +8 -0
  111. package/stack/local-logs.mjs +32 -0
  112. package/stack/local-ports.d.mts +7 -0
  113. package/stack/local-ports.mjs +39 -0
  114. package/stack/local-prepare.d.mts +22 -0
  115. package/stack/local-prepare.mjs +62 -0
  116. package/stack/local-run.d.mts +71 -0
  117. package/stack/local-run.mjs +200 -0
  118. package/stack/local-state.d.mts +35 -0
  119. package/stack/local-state.mjs +29 -0
  120. package/stack/local-status.d.mts +6 -0
  121. package/stack/local-status.mjs +36 -0
  122. package/stack/local-up.d.mts +11 -0
  123. package/stack/local-up.mjs +169 -0
  124. package/stack/local-verify.d.mts +6 -0
  125. package/stack/local-verify.mjs +21 -0
  126. package/stack/runtime-layers.d.mts +21 -0
  127. package/stack/runtime-layers.mjs +67 -0
  128. package/sync/hygiene.mjs +3 -1
  129. package/sync/index.d.mts +15 -12
  130. package/sync/index.mjs +114 -28
  131. package/templates/app/auth-doors.sql.partial +115 -0
  132. package/templates/app/ci-workflows/github/workflows/ci.yml +1 -1
  133. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +1 -1
  134. package/templates/app/module/jest.config.js.tpl +8 -0
  135. package/templates/app/module/src/index.ts.tpl +2 -0
  136. package/templates/app/module/src/manifest/declaration.spec.ts.tpl +11 -0
  137. package/templates/app/module/src/manifest/declaration.ts.tpl +13 -0
  138. package/templates/app/module/src/manifest/index.ts.tpl +1 -0
  139. package/templates/app/module/src/public/index.ts.tpl +2 -0
  140. package/templates/app/module/src/template/__name__.template.spec.ts.tpl +13 -0
  141. package/templates/app/module/src/template/__name__.template.ts.tpl +41 -0
  142. package/templates/app/module/src/template/index.ts.tpl +1 -0
  143. package/templates/app/module/src/workbench/components/__pascal__WorkbenchView.tsx.tpl +16 -0
  144. package/templates/app/module/src/workbench/index.ts.tpl +2 -0
  145. package/templates/app/package-scripts/core.json +1 -1
  146. package/templates/app/package-scripts/package.json +1 -1
  147. package/templates/app/package-scripts-lite/package.json +1 -1
  148. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +85 -70
  149. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +24 -12
  150. package/templates/app/skeleton-lite/.starcistacks/dev/infra/compose/compose.yaml +43 -0
  151. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +21 -26
  152. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +6 -0
  153. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +6 -1
  154. package/templates/app/skeleton-lite/db/migrations/__timestamp___baseline.sql +76 -13
  155. package/templates/be/auth-lite/src/features/api/auth/application/auth.contracts.ts +44 -0
  156. package/templates/be/auth-lite/src/features/api/auth/application/confirm.command.ts +11 -0
  157. package/templates/be/auth-lite/src/features/api/auth/application/confirm.handler.ts +22 -0
  158. package/templates/be/auth-lite/src/features/api/auth/application/forgot.command.ts +11 -0
  159. package/templates/be/auth-lite/src/features/api/auth/application/forgot.handler.ts +22 -0
  160. package/templates/be/auth-lite/src/features/api/auth/application/get-jwks.handler.ts +23 -0
  161. package/templates/be/auth-lite/src/features/api/auth/application/get-jwks.query.ts +12 -0
  162. package/templates/be/auth-lite/src/features/api/auth/application/google-begin.command.ts +11 -0
  163. package/templates/be/auth-lite/src/features/api/auth/application/google-begin.handler.ts +22 -0
  164. package/templates/be/auth-lite/src/features/api/auth/application/google-complete.command.ts +12 -0
  165. package/templates/be/auth-lite/src/features/api/auth/application/google-complete.handler.ts +23 -0
  166. package/templates/be/auth-lite/src/features/api/auth/application/login.command.ts +12 -0
  167. package/templates/be/auth-lite/src/features/api/auth/application/login.handler.ts +23 -0
  168. package/templates/be/auth-lite/src/features/api/auth/application/logout.command.ts +11 -0
  169. package/templates/be/auth-lite/src/features/api/auth/application/logout.handler.ts +22 -0
  170. package/templates/be/auth-lite/src/features/api/auth/application/refresh.command.ts +12 -0
  171. package/templates/be/auth-lite/src/features/api/auth/application/refresh.handler.ts +23 -0
  172. package/templates/be/auth-lite/src/features/api/auth/application/resend.command.ts +11 -0
  173. package/templates/be/auth-lite/src/features/api/auth/application/resend.handler.ts +22 -0
  174. package/templates/be/auth-lite/src/features/api/auth/application/reset.command.ts +11 -0
  175. package/templates/be/auth-lite/src/features/api/auth/application/reset.handler.ts +22 -0
  176. package/templates/be/auth-lite/src/features/api/auth/application/session.command.ts +12 -0
  177. package/templates/be/auth-lite/src/features/api/auth/application/session.handler.ts +23 -0
  178. package/templates/be/auth-lite/src/features/api/auth/application/signup.command.ts +11 -0
  179. package/templates/be/auth-lite/src/features/api/auth/application/signup.handler.ts +22 -0
  180. package/templates/be/auth-lite/src/features/api/auth/auth.module-definition.ts +6 -0
  181. package/templates/be/auth-lite/src/features/api/auth/auth.module.ts +21 -0
  182. package/templates/be/auth-lite/src/features/api/auth/auth.options.ts +3 -0
  183. package/templates/be/auth-lite/src/features/api/auth/index.ts +2 -0
  184. package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.module-definition.ts +6 -0
  185. package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.module.ts +9 -0
  186. package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.options.ts +3 -0
  187. package/templates/be/auth-lite/src/features/api/auth/transport/http/auth.controller.ts +145 -0
  188. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/forgot.request.ts +16 -0
  189. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/google-start.request.ts +17 -0
  190. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/login.request.ts +16 -0
  191. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/resend.request.ts +16 -0
  192. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/reset.request.ts +18 -0
  193. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/signup.request.ts +22 -0
  194. package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/token.request.ts +11 -0
  195. package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.spec.ts +46 -0
  196. package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.ts +19 -0
  197. package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.spec.ts +209 -0
  198. package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.ts +84 -0
  199. package/templates/be/auth-lite/src/modules/domain/identity/identity-email.log-events.ts +6 -0
  200. package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.spec.ts +233 -0
  201. package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.ts +176 -0
  202. package/templates/be/auth-lite/src/modules/domain/identity/identity-sign-in.log-events.ts +6 -0
  203. package/templates/be/auth-lite/src/modules/domain/identity/persistence/account-doors.port.ts +98 -0
  204. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-attempt.entity.ts +18 -0
  205. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-email-token.entity.ts +34 -0
  206. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-refresh-token.entity.ts +34 -0
  207. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-session.entity.ts +30 -0
  208. package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/index.ts +10 -0
  209. package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.spec.ts +86 -0
  210. package/templates/be/auth-lite/src/modules/domain/identity/rate-counter.service.ts +45 -0
  211. package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.spec.ts +97 -0
  212. package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.ts +81 -0
  213. package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.spec.ts +95 -0
  214. package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.ts +93 -0
  215. package/templates/be/auth-lite/src/tests/fixtures/builders/identity.builder.ts +40 -0
  216. package/templates/be/inbox/module-definition.ts.tpl +2 -9
  217. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +2 -11
  218. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +2 -9
  219. package/templates/be/patterns/api/feature.module-definition.ts.tpl +3 -9
  220. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +3 -9
  221. package/templates/be/patterns/cli/group.module-definition.ts.tpl +2 -9
  222. package/templates/be/patterns/event-bus/platform/event-bus.module-definition.ts.tpl +3 -10
  223. package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +2 -9
  224. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +2 -9
  225. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +3 -9
  226. package/templates/be/patterns/jobs/platform/jobs.module-definition.ts.tpl +3 -10
  227. package/templates/be/patterns/projections/projection.module-definition.ts.tpl +2 -9
  228. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +3 -10
  229. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +2 -9
  230. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +3 -9
  231. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +2 -9
  232. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +3 -9
  233. package/templates/be/patterns/realtime/channel.sse.module-definition.ts.tpl +3 -9
  234. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +3 -9
  235. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +3 -9
  236. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +2 -9
  237. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +3 -9
  238. package/templates/be/platform/graphql/graphql.module-definition.ts +2 -9
  239. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +3 -9
  240. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +3 -9
  241. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +2 -9
  242. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +3 -9
  243. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +2 -9
  244. package/templates/be/skeleton/src/modules/domain/identity/identity.module-definition.ts +2 -9
  245. package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +2 -9
  246. package/templates/be/skeleton/src/modules/domain/note/note.module-definition.ts +2 -9
  247. package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +2 -9
  248. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +2 -0
  249. package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +2 -9
  250. package/templates/be/skeleton/src/modules/platform/database/database.module-definition.ts +3 -13
  251. package/templates/be/skeleton/src/modules/platform/errors/errors.module-definition.ts +4 -11
  252. package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +4 -9
  253. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module-definition.ts +5 -11
  254. package/templates/be/skeleton/src/modules/platform/i18n/i18n.module-definition.ts +4 -11
  255. package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +2 -9
  256. package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +2 -9
  257. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +2 -9
  258. package/templates/be/skeleton-lite/apps/api/src/api.env.ts +4 -0
  259. package/templates/be/skeleton-lite/apps/api/src/api.options.ts +3 -0
  260. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +6 -5
  261. package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +3 -2
  262. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +12 -5
  263. package/templates/be/skeleton-lite/src/modules/domain/identity/errors/identity.error.ts +40 -0
  264. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +22 -1
  265. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +50 -2
  266. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +3 -0
  267. package/templates/be/skeleton-lite/src/modules/domain/identity/messages/identity-mail.messages.ts +65 -0
  268. package/templates/be/skeleton-lite/src/modules/domain/identity/messages/identity.messages.ts +28 -0
  269. package/templates/be/skeleton-lite/src/modules/integrations/mail/index.ts +6 -0
  270. package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.config.ts +33 -0
  271. package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.decorators.ts +14 -0
  272. package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.module-definition.ts +12 -0
  273. package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.module.ts +27 -0
  274. package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.options.ts +22 -0
  275. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/index.ts +3 -3
  276. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.config.ts +3 -1
  277. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.decorators.ts +8 -4
  278. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.module-definition.ts +5 -0
  279. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.module.ts +29 -0
  280. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.options.ts +6 -31
  281. package/templates/be/skeleton-lite/src/modules/integrations/passport/errors/passport.error.ts +18 -9
  282. package/templates/be/skeleton-lite/src/modules/integrations/passport/index.ts +35 -11
  283. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.spec.ts +44 -8
  284. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.config.ts +40 -18
  285. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.decorators.ts +18 -6
  286. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.kit.spec.ts +73 -0
  287. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.kit.ts +26 -0
  288. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.options.ts +24 -53
  289. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.registration.ts +31 -0
  290. package/templates/be/skeleton-lite/src/modules/platform/cqrs/index.ts +1 -1
  291. package/templates/be/skeleton-lite/src/modules/platform/database/entities.ts +2 -1
  292. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
  293. package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +1 -1
  294. package/templates/be/skeleton-lite/src/modules/platform/http-security/rate-limit.guard.spec.ts +11 -0
  295. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +22 -4
  296. package/templates/be/table/api/feature.module-definition.ts.tpl +3 -9
  297. package/templates/be/table/api/graphql.module-definition.ts.tpl +3 -9
  298. package/templates/be/table/module-definition.ts.tpl +2 -9
  299. package/templates/be/test-world/e2e/serves-once-migrated.e2e-spec.ts.tpl +9 -9
  300. package/templates/be/test-world/fixtures/migrations.sql.ts.tpl +4 -4
  301. package/templates/fe/auth-lite/apps/web/src/modules/auth/client.ts +26 -0
  302. package/templates/fe/auth-lite/apps/web/src/modules/auth/email-doors.ts +16 -0
  303. package/templates/fe/auth-lite/apps/web/src/modules/auth/index.ts +5 -0
  304. package/templates/fe/common/site-shell.tsx.partial +3 -3
  305. package/templates/fe/skeleton/apps/__app__/src/components/providers/AppProviders/index.tsx +33 -10
  306. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/index.ts +1 -0
  307. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useViewerId.ts +11 -0
  308. package/templates/fe/skeleton/apps/__app__/src/modules/toast/toast.tsx +15 -12
  309. package/templates/fe/skeleton/apps/__app__/src/modules/viewer-cache/index.ts +5 -0
  310. package/templates/fe/skeleton/apps/__app__/src/redux/slices/session.ts +1 -1
  311. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +30 -67
  312. package/templates/fe/skeleton/packages/__project__-i18n/src/proxy.ts +6 -21
  313. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +10 -16
  314. package/templates/fe/skeleton/packages/__project__-ui/src/composites/NotFoundNotice/index.tsx +4 -7
  315. package/templates/fe/skeleton/packages/__project__-ui/src/frames/SiteShell/index.tsx +2 -3
  316. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/confirm/page.tsx +9 -0
  317. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/forgot/page.tsx +9 -0
  318. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/resend/page.tsx +9 -0
  319. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/reset/page.tsx +9 -0
  320. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/signup/page.tsx +9 -0
  321. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +5 -9
  322. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/component.tsx +51 -0
  323. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/index.tsx +84 -0
  324. package/templates/fe/skeleton-lite/apps/web/src/components/composites/NotFoundNotice/index.tsx +4 -7
  325. package/templates/fe/skeleton-lite/apps/web/src/components/frames/SiteShell/index.tsx +1 -2
  326. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppConfirmPage/index.tsx +25 -0
  327. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppForgotPage/index.tsx +21 -0
  328. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/component.tsx +11 -3
  329. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +4 -1
  330. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppResendPage/index.tsx +21 -0
  331. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppResetPage/index.tsx +25 -0
  332. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppSignupPage/index.tsx +21 -0
  333. package/templates/fe/skeleton-lite/apps/web/src/hooks/emailAuth/emailAuth.shared.ts +12 -0
  334. package/templates/fe/skeleton-lite/apps/web/src/hooks/emailAuth/index.ts +2 -0
  335. package/templates/fe/skeleton-lite/apps/web/src/hooks/emailAuth/useEmailDoor.ts +32 -0
  336. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/index.ts +2 -2
  337. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +50 -1
  338. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +50 -1
  339. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +7 -30
  340. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +13 -14
  341. package/templates/fe/skeleton-lite/apps/web/src/modules/routes/index.ts +5 -0
  342. package/templates/fe/skeleton-lite/apps/web/src/proxy.ts +4 -17
  343. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +0 -16
  344. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +0 -11
  345. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +0 -7
  346. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +0 -6
  347. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +0 -8
  348. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +0 -6
  349. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +0 -186
  350. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.spec.ts +0 -22
  351. package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.client.ts +0 -25
  352. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.spec.ts +0 -104
  353. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.cookie.ts +0 -34
  354. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.spec.ts +0 -28
  355. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.password.ts +0 -11
  356. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.spec.ts +0 -88
  357. package/templates/be/skeleton-lite/src/modules/integrations/passport/passport.tokens.ts +0 -69
  358. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/google.strategy.ts +0 -41
  359. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/local.strategy.ts +0 -40
  360. package/templates/be/skeleton-lite/src/modules/integrations/passport/strategies/oauth-state.store.ts +0 -63
  361. package/templates/fe/skeleton/packages/__project__-ui/src/frames/SiteShell/classNames.ts +0 -4
  362. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/auth-door.ts +0 -30
  363. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +0 -50
  364. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/token-store.ts +0 -31
  365. package/templates/fe/skeleton-lite/apps/web/src/components/frames/SiteShell/classNames.ts +0 -4
@@ -0,0 +1,35 @@
1
+ /** What the environment recorded about itself. */
2
+ export interface LocalRecord {
3
+ processes: Record<string, {
4
+ pid: number;
5
+ startedAt: string;
6
+ port: number | null;
7
+ }>;
8
+ ports: Record<string, number>;
9
+ receipts: {
10
+ migrate?: {
11
+ at: string;
12
+ digest: string;
13
+ };
14
+ seed: Record<string, {
15
+ at: string;
16
+ digest: string;
17
+ }>;
18
+ setup: Record<string, {
19
+ at: string;
20
+ digest: string;
21
+ }>;
22
+ database?: {
23
+ at: string;
24
+ role: string;
25
+ };
26
+ };
27
+ }
28
+ /** The directory of one environment's state: `<state base>/stack/<project>/<env>`. */
29
+ export declare const stateDirectory: (env: NodeJS.ProcessEnv, project: string, environment: string) => string;
30
+ /** The log file of one process or service. */
31
+ export declare const logFile: (dir: string, name: string) => string;
32
+ /** Reads the record; an absent or damaged file is an empty record (the environment then reads as not started). */
33
+ export declare function readRecord(dir: string): LocalRecord;
34
+ /** Writes the record atomically; it holds ids, ports and digests, never a value. */
35
+ export declare function writeRecord(dir: string, record: LocalRecord): void;
@@ -0,0 +1,29 @@
1
+ // local-state.mjs - where a dev environment keeps what it started: process ids, logs and receipts, under the per-host state base
2
+ // (STARCI_LOCAL_ROOT, else the runtime's .runtime), never in the app's checkout (README "Harness state lives in the runtime").
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { starciLocalRoot } from '../runtime/engine/runtime-root.mjs';
6
+ const empty = () => ({ processes: {}, ports: {}, receipts: { seed: {}, setup: {} } });
7
+ /** The directory of one environment's state: `<state base>/stack/<project>/<env>`. */
8
+ export const stateDirectory = (env, project, environment) => path.join(starciLocalRoot(env), 'stack', project, environment);
9
+ /** The log file of one process or service. */
10
+ export const logFile = (dir, name) => path.join(dir, 'logs', `${name}.log`);
11
+ /** Reads the record; an absent or damaged file is an empty record (the environment then reads as not started). */
12
+ export function readRecord(dir) {
13
+ try {
14
+ const parsed = JSON.parse(fs.readFileSync(path.join(dir, 'state.json'), 'utf8'));
15
+ const base = empty();
16
+ return { processes: parsed.processes ?? base.processes, ports: parsed.ports ?? base.ports, receipts: { ...base.receipts, ...parsed.receipts } };
17
+ }
18
+ catch {
19
+ return empty();
20
+ }
21
+ }
22
+ /** Writes the record atomically; it holds ids, ports and digests, never a value. */
23
+ export function writeRecord(dir, record) {
24
+ fs.mkdirSync(path.join(dir, 'logs'), { recursive: true });
25
+ const target = path.join(dir, 'state.json');
26
+ const temporary = `${target}.${process.pid}.tmp`;
27
+ fs.writeFileSync(temporary, `${JSON.stringify(record, null, 2)}\n`);
28
+ fs.renameSync(temporary, target);
29
+ }
@@ -0,0 +1,6 @@
1
+ import type { StackContext } from './context.mjs';
2
+ import type { ActionResult } from './remote.mjs';
3
+ import type { LocalConfig } from './local-config.mjs';
4
+ import type { LocalDeps } from './local-run.mjs';
5
+ /** `status` for a dev environment. */
6
+ export declare function statusLocal(ctx: StackContext, local: LocalConfig, _options?: Record<string, unknown>, deps?: LocalDeps): Promise<ActionResult>;
@@ -0,0 +1,36 @@
1
+ import { firstLine } from './context.mjs';
2
+ import { migrationsDigest } from './local-up.mjs';
3
+ import { compose, isAlive, probe, stateOf } from './local-run.mjs';
4
+ import { readRecord } from './local-state.mjs';
5
+ /** `status` for a dev environment. */
6
+ export async function statusLocal(ctx, local, _options = {}, deps = {}) {
7
+ const record = readRecord(stateOf(ctx, local));
8
+ const lines = [];
9
+ let ok = true;
10
+ const mark = (good, text) => { ok &&= good; lines.push(`${good ? 'ok ' : 'FAIL '}${text}`); };
11
+ for (const service of local.services) {
12
+ const id = firstLine((await compose(ctx, local, ['ps', '-q', service.name])).stdout);
13
+ if (!id) {
14
+ mark(false, `service ${service.name}: not created`);
15
+ continue;
16
+ }
17
+ const inspected = ctx.spawn('docker', ['inspect', '-f', '{{.State.Status}} {{if .State.Health}}{{.State.Health.Status}}{{else}}nohealth{{end}}', id]);
18
+ const [state, health] = firstLine(inspected.stdout).split(' ');
19
+ mark(state === 'running' && (service.wait !== 'healthy' || health === 'healthy'), `service ${service.name}: ${state}${health === 'nohealth' ? '' : `, ${health}`}`);
20
+ }
21
+ const alive = deps.alive ?? isAlive;
22
+ for (const run of local.processes) {
23
+ const entry = record.processes[run.name];
24
+ mark(entry !== undefined && alive(entry.pid), entry ? `process ${run.name}: pid ${entry.pid} ${alive(entry.pid) ? 'alive' : 'gone'}` : `process ${run.name}: not started`);
25
+ }
26
+ if (local.database) {
27
+ const receipt = record.receipts.migrate;
28
+ const current = migrationsDigest(local.repoRoot);
29
+ mark(receipt?.digest === current, receipt ? `migrations: receipt ${receipt.digest} ${receipt.digest === current ? 'is current' : `is stale, the folder is ${current}`}` : 'migrations: no receipt');
30
+ }
31
+ for (const row of local.ready) {
32
+ const result = await probe(ctx, local, row);
33
+ mark(result.ok, `ready ${result.detail}`);
34
+ }
35
+ return { ok, lines, data: { processes: record.processes } };
36
+ }
@@ -0,0 +1,11 @@
1
+ import type { StackContext } from './context.mjs';
2
+ import type { ActionResult } from './remote.mjs';
3
+ import type { LocalConfig } from './local-config.mjs';
4
+ import type { LocalDeps } from './local-run.mjs';
5
+ /** The digest of the migration folder (names and contents): what a migrate receipt records. */
6
+ export declare function migrationsDigest(repoRoot: string): string;
7
+ /** `up` for a dev environment. */
8
+ export declare function upLocal(ctx: StackContext, local: LocalConfig, { execute, timeoutSeconds }?: {
9
+ execute?: boolean;
10
+ timeoutSeconds?: number;
11
+ }, deps?: LocalDeps): Promise<ActionResult>;
@@ -0,0 +1,169 @@
1
+ // local-up.mjs - `starci app stack up --env <dev env> [--execute]`: the one command that brings the whole dev environment up (PROJECT-STACKS-6).
2
+ // Order: host prerequisites, dev custody, setup (install, codegen), compose services and their health, the environment's own database role,
3
+ // migrations with a receipt, seed, native app processes, readiness, origins. Idempotent: what runs is kept, what changed is converged.
4
+ // A dry run prints the plan. It never generates a replacement secret, never removes a volume and touches only its own compose project.
5
+ import crypto from 'node:crypto';
6
+ import fs from 'node:fs';
7
+ import path from 'node:path';
8
+ import { firstLine } from './context.mjs';
9
+ import { failure, refusal } from './errors.mjs';
10
+ import { missingKeys, missingStep, openCustody } from './local-custody.mjs';
11
+ import { hostRefusal, preflight, runSetup } from './local-prepare.mjs';
12
+ import { DEFAULT_TIMEOUT_SECONDS, makeDataDirectories, commandDigest, compose, composeValues, databaseUrl, isAlive, probe, runCommand, runEnvironment, serviceReady, startProcess, stateOf, waitFor } from './local-run.mjs';
13
+ import { readRecord, writeRecord } from './local-state.mjs';
14
+ import { ensureRuntimeLayers, layerPlan } from './runtime-layers.mjs';
15
+ const SAFE_PASSWORD = /^[A-Za-z0-9_-]{8,}$/u;
16
+ /** The digest of the migration folder (names and contents): what a migrate receipt records. */
17
+ export function migrationsDigest(repoRoot) {
18
+ const directory = path.join(repoRoot, 'db', 'migrations');
19
+ const hash = crypto.createHash('sha256');
20
+ let names = [];
21
+ try {
22
+ names = fs.readdirSync(directory).filter((name) => name.endsWith('.sql')).sort();
23
+ }
24
+ catch {
25
+ return 'none';
26
+ }
27
+ for (const name of names)
28
+ hash.update(name).update(fs.readFileSync(path.join(directory, name)));
29
+ return hash.digest('hex').slice(0, 16);
30
+ }
31
+ /** Creates the environment's own login role and database if absent and sets its password (psql in the db container, SQL on stdin, never argv). */
32
+ async function provisionDatabase(ctx, local, custody) {
33
+ const database = local.database;
34
+ if (!database)
35
+ return 'database: none declared';
36
+ const password = custody.values.get(database.password.key) ?? '';
37
+ if (!SAFE_PASSWORD.test(password))
38
+ throw refusal('STACK_LOCAL_DB_PASSWORD', `${database.password.key} must be at least 8 characters of letters, digits, dash and underscore (the generated password kind is); the owner replaces it with secret unset and secret init`);
39
+ const sql = [
40
+ `DO $do$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '${database.role}') THEN CREATE ROLE "${database.role}" LOGIN SUPERUSER PASSWORD '${password}'; ELSE ALTER ROLE "${database.role}" LOGIN SUPERUSER PASSWORD '${password}'; END IF; END $do$;`,
41
+ `SELECT 'CREATE DATABASE "${database.name}" OWNER "${database.role}"' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '${database.name}')\\gexec`,
42
+ '',
43
+ ].join('\n');
44
+ const result = await compose(ctx, local, ['exec', '-T', database.service, 'psql', '-v', 'ON_ERROR_STOP=1', '-U', database.adminUser, '-d', 'postgres'], { values: composeValues(local, custody), input: sql });
45
+ if (result.status !== 0)
46
+ throw failure('STACK_LOCAL_DB_ROLE', `could not provision the role ${database.role}: ${(result.stderr.trim().split(/\r?\n/u)[0] ?? '').replaceAll(password, '[redacted]')}`);
47
+ return `database: role ${database.role} and database ${database.name} ready on port ${local.ports[database.port]}`;
48
+ }
49
+ /** Gives the app role the migrations created its own password for this environment (cluster roles are never shared: the db container is this environment's alone). */
50
+ async function appRolePassword(ctx, local, custody) {
51
+ const database = local.database;
52
+ if (!database?.appRole || !database.appPassword)
53
+ return '';
54
+ const password = custody.values.get(database.appPassword.key) ?? '';
55
+ if (!SAFE_PASSWORD.test(password))
56
+ throw refusal('STACK_LOCAL_DB_PASSWORD', `${database.appPassword.key} must be at least 8 characters of letters, digits, dash and underscore`);
57
+ const sql = `DO $do$ BEGIN IF EXISTS (SELECT FROM pg_roles WHERE rolname = '${database.appRole}') THEN ALTER ROLE "${database.appRole}" LOGIN PASSWORD '${password}'; ELSE RAISE EXCEPTION 'role ${database.appRole} does not exist: the migrations create it'; END IF; END $do$;\n`;
58
+ const result = await compose(ctx, local, ['exec', '-T', database.service, 'psql', '-v', 'ON_ERROR_STOP=1', '-U', database.adminUser, '-d', database.name], { values: composeValues(local, custody), input: sql });
59
+ if (result.status !== 0)
60
+ throw failure('STACK_LOCAL_DB_ROLE', `could not set the password of ${database.appRole}: ${(result.stderr.trim().split(/\r?\n/u)[0] ?? '').replaceAll(password, '[redacted]')}`);
61
+ return `, ${database.appRole} password set`;
62
+ }
63
+ /** Migrates through `starci app db migrate` in this process; the URL stays in the environment of that call. */
64
+ async function migrate(ctx, local, custody, deps) {
65
+ const database = local.database;
66
+ const url = databaseUrl(local, custody);
67
+ if (!database || url === null)
68
+ return 'migrate: no database declared';
69
+ const container = firstLine((await compose(ctx, local, ['ps', '-q', database.service])).stdout);
70
+ const env = { ...ctx.env, DATABASE_URL: url, STARCI_DB_CONTAINER: container };
71
+ const run = deps.migrate ?? (async (urlEnv, environment, cwd) => {
72
+ const { main } = await import('../src/main.mjs');
73
+ return main(['db', 'migrate', '--db-url-env', urlEnv, '--execute'], { cwd, env: environment, stdout: () => { }, stderr: () => { } });
74
+ });
75
+ const code = await run('DATABASE_URL', env, local.repoRoot);
76
+ if (code !== 0)
77
+ throw failure('STACK_LOCAL_MIGRATE', `starci app db migrate exited ${code}; run it with --db-url-env DATABASE_URL to read its report`);
78
+ const dir = stateOf(ctx, local);
79
+ const record = readRecord(dir);
80
+ record.receipts.migrate = { at: new Date(ctx.now()).toISOString(), digest: migrationsDigest(local.repoRoot) };
81
+ writeRecord(dir, record);
82
+ return `migrate: applied, receipt ${record.receipts.migrate.digest}${await appRolePassword(ctx, local, custody)}`;
83
+ }
84
+ /** Runs the seed commands once each (a receipt keyed by the command's digest); the seed owns its idempotence beyond that. */
85
+ async function seed(ctx, local, custody, after) {
86
+ const dir = stateOf(ctx, local);
87
+ const record = readRecord(dir);
88
+ const out = [];
89
+ for (const run of local.seed.filter((item) => item.after === after)) {
90
+ if (record.receipts.seed[run.name]?.digest === commandDigest(run)) {
91
+ out.push(`seed ${run.name}: already done`);
92
+ continue;
93
+ }
94
+ await runCommand(ctx, local, run, runEnvironment(ctx, local, run, custody, 'owner'));
95
+ record.receipts.seed[run.name] = { at: new Date(ctx.now()).toISOString(), digest: commandDigest(run) };
96
+ writeRecord(dir, record);
97
+ out.push(`seed ${run.name}: done`);
98
+ }
99
+ return out;
100
+ }
101
+ /** Starts each native process that is not already running. */
102
+ function startProcesses(ctx, local, custody, deps) {
103
+ const dir = stateOf(ctx, local);
104
+ const record = readRecord(dir);
105
+ const out = [];
106
+ const alive = deps.alive ?? isAlive;
107
+ for (const run of local.processes) {
108
+ const known = record.processes[run.name];
109
+ if (known && alive(known.pid)) {
110
+ out.push(`process ${run.name}: running (pid ${known.pid})`);
111
+ continue;
112
+ }
113
+ const pid = startProcess(ctx, local, run, runEnvironment(ctx, local, run, custody, 'app'));
114
+ record.processes[run.name] = { pid, startedAt: new Date(ctx.now()).toISOString(), port: run.port === null ? null : (local.ports[run.port] ?? null) };
115
+ out.push(`process ${run.name}: started (pid ${pid})`);
116
+ }
117
+ record.ports = { ...local.ports };
118
+ writeRecord(dir, record);
119
+ return out;
120
+ }
121
+ /** The plan a dry run prints. */
122
+ function plan(local, missing) {
123
+ return [
124
+ `plan custody: ${missing.length ? `missing ${missing.join(', ')}: ${missingStep(local, missing)}` : 'open the sealed dev values into host-local files'}`,
125
+ ...local.setup.map((run) => `plan setup ${run.name}: ${run.argv.join(' ')}`),
126
+ ...layerPlan(local),
127
+ `plan services: ${local.services.map((service) => `${service.name} (wait ${service.wait})`).join(', ')}`,
128
+ ...(local.database ? [`plan database: own role ${local.database.role}, database ${local.database.name}, then starci app db migrate`] : []),
129
+ ...local.seed.map((run) => `plan seed ${run.name}: ${run.argv.join(' ')}`),
130
+ ...local.processes.map((run) => `plan process ${run.name}: ${run.argv.join(' ')}`),
131
+ ...local.ready.map((row) => `plan ready ${row.name}: ${row.url}`),
132
+ ];
133
+ }
134
+ /** `up` for a dev environment. */
135
+ export async function upLocal(ctx, local, { execute = false, timeoutSeconds = DEFAULT_TIMEOUT_SECONDS } = {}, deps = {}) {
136
+ const host = await preflight(ctx, local, deps);
137
+ if (!host.ok)
138
+ return hostRefusal(host.lines);
139
+ const missing = missingKeys(local);
140
+ if (!execute)
141
+ return { ok: missing.length === 0, lines: [...host.lines, ...plan(local, missing), 'dry run: nothing was started; arm it with --execute'], data: { rows: host.rows } };
142
+ const lines = [...host.lines];
143
+ const custody = openCustody(ctx, local);
144
+ lines.push(`custody: ${custody.values.size} key file(s) in the host-local directory`);
145
+ lines.push(...await runSetup(ctx, local, custody));
146
+ makeDataDirectories(ctx, local);
147
+ lines.push(...await ensureRuntimeLayers(ctx, local));
148
+ const values = composeValues(local, custody);
149
+ const started = await compose(ctx, local, ['up', '-d', ...local.services.map((service) => service.name)], { values, heavy: true, label: 'compose up' });
150
+ if (started.status !== 0)
151
+ throw failure('STACK_LOCAL_COMPOSE', `docker compose up failed: ${(started.stderr.trim().split(/\r?\n/u)[0] ?? '').slice(0, 200)}`);
152
+ const deadline = ctx.now() + timeoutSeconds * 1000;
153
+ for (const service of local.services) {
154
+ const ready = await serviceReady(ctx, local, service.name, deadline, deps);
155
+ lines.push(`service ${ready.detail}`);
156
+ if (!ready.ok)
157
+ throw failure('STACK_LOCAL_SERVICE', `${service.name} did not become ready in ${timeoutSeconds}s (${ready.detail}); read it with: starci app stack logs --env ${local.env} --service ${service.name}`);
158
+ }
159
+ lines.push(await provisionDatabase(ctx, local, custody), await migrate(ctx, local, custody, deps), ...await seed(ctx, local, custody, 'migrate'), ...startProcesses(ctx, local, custody, deps));
160
+ for (const row of local.ready) {
161
+ const result = await waitFor(ctx, deadline, () => probe(ctx, local, row));
162
+ lines.push(`${result.ok ? 'ok ' : 'FAIL '}ready ${result.detail}`);
163
+ if (!result.ok)
164
+ throw failure('STACK_LOCAL_READY', `${row.name} is not ready: ${result.detail}; read a process log with: starci app stack logs --env ${local.env} --service ${local.processes.map((run) => run.name).join('|')}`);
165
+ }
166
+ lines.push(...await seed(ctx, local, custody, 'ready'));
167
+ lines.push(...local.origins.map((origin) => `origin ${origin.name}: ${origin.url.replaceAll(/\$\{port\.([a-z][a-z0-9-]*)\}/gu, (_whole, name) => String(local.ports[name]))}`));
168
+ return { ok: true, lines, data: { origins: local.origins } };
169
+ }
@@ -0,0 +1,6 @@
1
+ import type { StackContext } from './context.mjs';
2
+ import type { ActionResult } from './remote.mjs';
3
+ import type { LocalConfig } from './local-config.mjs';
4
+ import type { LocalDeps } from './local-run.mjs';
5
+ /** `verify` for a dev environment. */
6
+ export declare function verifyLocal(ctx: StackContext, local: LocalConfig, _options?: Record<string, unknown>, deps?: LocalDeps): Promise<ActionResult>;
@@ -0,0 +1,21 @@
1
+ import { refusal } from './errors.mjs';
2
+ import { isAlive, probe, stateOf } from './local-run.mjs';
3
+ import { readRecord } from './local-state.mjs';
4
+ /** `verify` for a dev environment. */
5
+ export async function verifyLocal(ctx, local, _options = {}, deps = {}) {
6
+ if (local.verify.length === 0)
7
+ throw refusal('STACK_LOCAL_VERIFY_UNDECLARED', `environment ${local.env} declares no local.verify journey: a liveness probe alone is not verification`);
8
+ const record = readRecord(stateOf(ctx, local));
9
+ const alive = deps.alive ?? isAlive;
10
+ const down = local.processes.filter((run) => !(record.processes[run.name] && alive(record.processes[run.name].pid)));
11
+ if (down.length)
12
+ return { ok: false, lines: [`FAIL ${down.map((run) => run.name).join(', ')} not running: start the environment with starci app stack up --env ${local.env} --execute`] };
13
+ const lines = [];
14
+ let ok = true;
15
+ for (const row of local.verify) {
16
+ const result = await probe(ctx, local, row);
17
+ ok &&= result.ok;
18
+ lines.push(`${result.ok ? 'ok ' : 'FAIL '}${result.detail}`);
19
+ }
20
+ return { ok, lines, data: { checked: local.verify.length } };
21
+ }
@@ -0,0 +1,21 @@
1
+ import type { StackContext } from './context.mjs';
2
+ import type { LocalConfig } from './local-config.mjs';
3
+ /** The registry path of the harness-owned layers: `<prefix><layer>`. */
4
+ export declare const LAYER_PREFIX = "ghcr.io/starci183/";
5
+ /** The layers whose recipe this package ships. */
6
+ export declare const SHIPPED_LAYERS: readonly string[];
7
+ /** The folder holding the recipe of a layer in this package. */
8
+ export declare const recipeDirectory: (layer: string) => string;
9
+ /** One layer an app image builds on: the layer, the reference as the Dockerfile writes it and the tag a local build gives it. */
10
+ export interface LayerUse {
11
+ layer: string;
12
+ reference: string;
13
+ tag: string;
14
+ dockerfile: string;
15
+ }
16
+ /** The layers `.starcistacks/images/*` of the app builds on (each distinct reference once), from the `FROM` lines of its Dockerfiles. */
17
+ export declare function layersUsed(repoRoot: string): LayerUse[];
18
+ /** The lines of the plan a dry run prints for the layers the app builds on. */
19
+ export declare const layerPlan: (local: LocalConfig) => string[];
20
+ /** Builds every layer the app names that the host neither holds nor can pull; returns one line per layer. */
21
+ export declare function ensureRuntimeLayers(ctx: StackContext, local: LocalConfig): Promise<string[]>;
@@ -0,0 +1,67 @@
1
+ // runtime-layers.mjs - the harness runtime layers a dev stack builds on (images/README.md "The OpenClaw runtime layer"; R374 STACKS_IMAGE_BASE_NOT_FORK).
2
+ // An app image folder `.starcistacks/images/<name>/Dockerfile` is `FROM ghcr.io/starci183/<layer>:<tag>[@sha256:<digest>]`. While the harness has not
3
+ // published that layer (or the host cannot pull it), `starci app stack up` builds it from the recipe this package ships (stack/images/<layer>/,
4
+ // byte-equal to the harness repository's images/<layer>/, which `npm run bundles` copies and a spec compares) and tags it with the reference the
5
+ // app names minus its digest, so the compose build of the app image finds it in the local store. The build is a governed heavy command
6
+ // (hard rule 8): an agent never runs docker by hand. A reference whose image the host already holds, or can pull, is left alone.
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { fileURLToPath } from 'node:url';
10
+ import { failure } from './errors.mjs';
11
+ /** The registry path of the harness-owned layers: `<prefix><layer>`. */
12
+ export const LAYER_PREFIX = 'ghcr.io/starci183/';
13
+ /** The layers whose recipe this package ships. */
14
+ export const SHIPPED_LAYERS = Object.freeze(['openclaw-runtime']);
15
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
16
+ /** The folder holding the recipe of a layer in this package. */
17
+ export const recipeDirectory = (layer) => path.join(HERE, 'images', layer);
18
+ /** The layers `.starcistacks/images/*` of the app builds on (each distinct reference once), from the `FROM` lines of its Dockerfiles. */
19
+ export function layersUsed(repoRoot) {
20
+ const folder = path.join(repoRoot, '.starcistacks', 'images');
21
+ if (!fs.existsSync(folder))
22
+ return [];
23
+ const found = new Map();
24
+ for (const entry of fs.readdirSync(folder, { withFileTypes: true }).filter((item) => item.isDirectory()).sort((a, b) => (a.name < b.name ? -1 : 1))) {
25
+ const dockerfile = path.join(folder, entry.name, 'Dockerfile');
26
+ if (!fs.existsSync(dockerfile))
27
+ continue;
28
+ for (const line of fs.readFileSync(dockerfile, 'utf8').split(/\r?\n/u)) {
29
+ const from = /^\s*FROM\s+(?:--\S+\s+)*(\S+)/iu.exec(line)?.[1];
30
+ if (from?.startsWith(LAYER_PREFIX) !== true)
31
+ continue;
32
+ const reference = from;
33
+ const tag = reference.replace(/@sha256:[0-9a-f]+$/u, '');
34
+ const layer = tag.slice(LAYER_PREFIX.length).replace(/:.*$/u, '');
35
+ if (SHIPPED_LAYERS.includes(layer) && !found.has(reference))
36
+ found.set(reference, { layer, reference, tag: tag.includes(':') ? tag : `${tag}:latest`, dockerfile: path.relative(repoRoot, dockerfile).split(path.sep).join('/') });
37
+ }
38
+ }
39
+ return [...found.values()];
40
+ }
41
+ /** Whether the host holds `reference` or can pull it: the local store first, then the registry's manifest. */
42
+ async function available(ctx, use) {
43
+ if ((await ctx.spawn('docker', ['image', 'inspect', use.tag])).status === 0)
44
+ return true;
45
+ return (await ctx.spawn('docker', ['manifest', 'inspect', use.reference])).status === 0;
46
+ }
47
+ /** The lines of the plan a dry run prints for the layers the app builds on. */
48
+ export const layerPlan = (local) => layersUsed(local.repoRoot).map((use) => `plan runtime layer ${use.layer}: ${use.tag} is built from the shipped recipe when the host holds none and the registry has none`);
49
+ /** Builds every layer the app names that the host neither holds nor can pull; returns one line per layer. */
50
+ export async function ensureRuntimeLayers(ctx, local) {
51
+ const lines = [];
52
+ for (const use of layersUsed(local.repoRoot)) {
53
+ if (await available(ctx, use)) {
54
+ lines.push(`runtime layer ${use.layer}: ${use.tag} is available`);
55
+ continue;
56
+ }
57
+ const recipe = recipeDirectory(use.layer);
58
+ if (!fs.existsSync(path.join(recipe, 'Dockerfile')))
59
+ throw failure('STACK_LOCAL_RUNTIME_LAYER', `${use.dockerfile} builds on ${use.reference}, which is not published, and this package ships no recipe for ${use.layer}`);
60
+ const argv = ['docker', 'build', '-t', use.tag, recipe];
61
+ const built = ctx.heavy ? await ctx.heavy({ kind: 'build', label: `${local.project} runtime layer ${use.layer}`, cwd: local.repoRoot, argv, env: ctx.env }) : await ctx.spawn('docker', argv.slice(1), { cwd: local.repoRoot, env: ctx.env });
62
+ if (built.status !== 0)
63
+ throw failure('STACK_LOCAL_RUNTIME_LAYER', `docker build of the runtime layer ${use.layer} failed: ${(built.stderr.trim().split(/\r?\n/u)[0] ?? '').slice(0, 200)}`);
64
+ lines.push(`runtime layer ${use.layer}: ${use.reference} is not published; built ${use.tag} from the recipe this package ships`);
65
+ }
66
+ return lines;
67
+ }
package/sync/hygiene.mjs CHANGED
@@ -45,6 +45,8 @@ const PLAINTEXT_NAME = /(^|\/)(\.env(\..*)?|[^/]*\.(pem|key|identity|age))$/;
45
45
  /** The app root's work tree and stack tree, app-relative (`starci app check --staged` runs at the app root). */
46
46
  const WORK = '.starciwork/';
47
47
  const STACKS = '.starcistacks/';
48
+ /** The committed check cache: written by the CLI, judged by what its files hold (secret-records.mjs through the secret judgement), never by a `secrets/` folder in its path. */
49
+ const CACHE = '.starcistacks/cache/';
48
50
  const GUARDED = (file) => file.startsWith(WORK) || file.startsWith(STACKS);
49
51
  /** The subset of `files` git ignores (as if untracked), asked in one call: a Set of paths. */
50
52
  function ignoredAmong(cwd, files) {
@@ -67,7 +69,7 @@ export function hygieneFindings(files, ignored) {
67
69
  if (file.startsWith(WORK) && ignored.has(file)) {
68
70
  findings.push({ file, code: 'HFS_WORK_AGENT_DATA', message: 'is agent output, which .starciwork/.gitignore refuses; keep it in the scratchpad or the blob store' });
69
71
  }
70
- if (file.startsWith(STACKS) && !/\.enc(?:\.yaml)?$/.test(file) && !file.endsWith('.env.example') && !file.endsWith('/secrets/.gitkeep')) {
72
+ if (file.startsWith(STACKS) && !file.startsWith(CACHE) && !/\.enc(?:\.yaml)?$/.test(file) && !file.endsWith('.env.example') && !file.endsWith('/secrets/.gitkeep')) {
71
73
  if (file.includes('/secrets/'))
72
74
  findings.push({ file, code: 'HFS_PLAINTEXT_SECRET', message: 'sits under secrets/ but is not sealed; only <slug>.enc may be tracked' });
73
75
  else if (PLAINTEXT_NAME.test(file))
package/sync/index.d.mts CHANGED
@@ -64,6 +64,8 @@ export interface AppSourceFacts {
64
64
  read: (file: string) => string;
65
65
  workPresent: boolean;
66
66
  hasImage: (file: string) => boolean;
67
+ modules?: readonly string[];
68
+ otherPackages?: readonly string[];
67
69
  stack: any;
68
70
  quality: any;
69
71
  badges: string;
@@ -138,12 +140,8 @@ export declare function sonarScope(presets: Presets | null | undefined): {
138
140
  tests: string[];
139
141
  exclusions: string[];
140
142
  };
141
- /**
142
- * The be source of the app at `root` the coverage components read: { files, read }, `files` be-relative posix paths of every `*.ts` file under
143
- * be/apps and be/src (build output and dependencies skipped) and `read` their text. `workPresent` records whether the optional
144
- * lite brand tree exists, so sync can preserve its absence. `hasImage` identifies authored optional Dockerfiles without reading
145
- * their content. An app with no be folder has no source files.
146
- */
143
+ /** The managed ESLint config at the app root that lints the root workspace packages. */
144
+ export declare const PACKAGES_LINT_FILE = "eslint.config.mjs";
147
145
  export declare function appSource(root: string, { parseYaml, notices }?: {
148
146
  parseYaml?: ParseYaml | undefined;
149
147
  notices?: string[] | undefined;
@@ -178,7 +176,7 @@ export declare function jestCoverageSource(coverage: {
178
176
  * `docker:build:<app>` builds the image of one app (the exact command its Dockerfile header states, tag `<project>/<app>:dev`) and
179
177
  * `docker:build` builds every image, one after the other, never pushing.
180
178
  */
181
- export declare function appScripts(app: ResolvedApp, source: Partial<Pick<AppSourceFacts, 'hasImage'>> | undefined): string;
179
+ export declare function appScripts(app: ResolvedApp, source: Partial<Pick<AppSourceFacts, 'hasImage' | 'modules'>> | undefined): string;
182
180
  /** The stylesheets of the fe side the CSS canon judges, relative to fe/: every app and every workspace package (slot fe.route, fe.package.*). */
183
181
  export declare const STYLE_GLOB = "{apps,packages}/*/src/**/*.css";
184
182
  /** The apps images.yml builds (one matrix entry per declared app): name and Dockerfile, be then fe, then a lite app's db image. */
@@ -192,14 +190,13 @@ export declare function imageFiles(app: ResolvedApp, source?: Partial<Pick<AppSo
192
190
  path: string;
193
191
  content: string;
194
192
  }[];
195
- /** Every value a template of `scope` can name, derived from hfs.json and the presets. */
196
193
  export declare function variables(app: ResolvedApp, scope: string, presets: Presets | null | undefined, sonarKey: string | null | undefined, manifest?: SlotManifest, source?: Pick<AppSourceFacts, 'files' | 'read'> & Partial<AppSourceFacts>): Record<string, string>;
197
194
  /**
198
- * The managed content of every target of the app: [{ path, mode, content, hash, scope }], paths app-relative (a side's targets under
199
- * its folder); a scripts target also carries `scripts`, the parsed block. `sonarKey` is the key the be side's stack declaration
200
- * names; without one the key is the project.
195
+ * The rendered targets of an app declaration. Without source facts the app counts the back-end source files its own targets
196
+ * render (the managed sign-in glue), as a check reading the disk after the targets are written does, so a render of a declaration
197
+ * alone is already in sync with itself.
201
198
  */
202
- export declare function renderTargets(hfs: unknown, presets: Presets | null | undefined, { sonarKey, readTemplate, manifest, source }?: {
199
+ export declare function renderTargets(hfs: unknown, presets: Presets | null | undefined, options?: {
203
200
  sonarKey?: string | null | undefined;
204
201
  readTemplate?: (name: string) => string;
205
202
  manifest?: SlotManifest;
@@ -212,6 +209,12 @@ export declare function writtenContent(target: RenderedTarget, existing: string
212
209
  export declare function writeTargets(root: string, targets: readonly RenderedTarget[]): string[];
213
210
  /** The hfs.json object of the app at `root` (validated), or a SyncError. */
214
211
  export declare function loadHfs(root: string, manifest?: SlotManifest): Record<string, any>;
212
+ /**
213
+ * The source facts as they stand once the rendered back-end source files are on disk: a managed `.ts` target of the be side counts as a
214
+ * file of the app (and reads as its render), so the CI shard count and the coverage scope that count or read the source name the same
215
+ * files before and after the targets are written, and one sync leaves the app in sync.
216
+ */
217
+ export declare function settleSource(source: AppSourceFacts, targets: readonly RenderedTarget[]): AppSourceFacts;
215
218
  /** The rendered targets of the app at `root`: its hfs.json, the Sonar key the be side's stack declaration names, the presets it installs. */
216
219
  export declare function renderRepo(root: string, { presets, parseYaml, notices, manifest }?: {
217
220
  presets?: Presets | undefined;