okengine 0.10.3 → 0.11.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 (318) hide show
  1. package/package.json +76 -10
  2. package/site/content/docs/deployment/docker-swarm.mdx +48 -111
  3. package/site/content/docs/deployment/docker.mdx +43 -36
  4. package/site/content/docs/deployment/index.mdx +9 -9
  5. package/site/content/docs/deployment/kubernetes.mdx +4 -4
  6. package/site/content/docs/elements/ai.mdx +7 -7
  7. package/site/content/docs/elements/channel.mdx +9 -7
  8. package/site/content/docs/elements/clock.mdx +8 -8
  9. package/site/content/docs/elements/flow.mdx +46 -47
  10. package/site/content/docs/elements/gate.mdx +6 -5
  11. package/site/content/docs/elements/signal.mdx +2 -2
  12. package/site/content/docs/elements/store.mdx +71 -78
  13. package/site/content/docs/elements/vault.mdx +89 -70
  14. package/site/content/docs/get-started/basic-usage.mdx +8 -8
  15. package/site/content/docs/get-started/installation.mdx +36 -36
  16. package/site/content/docs/get-started/introduction.mdx +11 -0
  17. package/site/content/docs/get-started/why.mdx +2 -2
  18. package/site/content/docs/plugins/anonymous.mdx +1 -1
  19. package/site/content/docs/plugins/magic-link.mdx +11 -11
  20. package/site/content/docs/plugins/otp.mdx +2 -2
  21. package/site/content/docs/plugins/passkey.mdx +1 -1
  22. package/site/content/docs/plugins/two-factor.mdx +1 -1
  23. package/site/content/docs/plugins/username.mdx +1 -1
  24. package/site/content/docs/recipes/caddy.mdx +1 -1
  25. package/site/content/docs/recipes/cockroachdb.mdx +2 -2
  26. package/site/content/docs/recipes/dragonfly.mdx +1 -1
  27. package/site/content/docs/recipes/index.mdx +1 -2
  28. package/site/content/docs/recipes/llama-cpp.mdx +21 -23
  29. package/site/content/docs/recipes/mailpit.mdx +11 -11
  30. package/site/content/docs/recipes/meilisearch.mdx +10 -10
  31. package/site/content/docs/recipes/meta.json +0 -1
  32. package/site/content/docs/recipes/ollama.mdx +1 -1
  33. package/site/content/docs/recipes/pgdog.mdx +12 -9
  34. package/site/content/docs/recipes/postgres.mdx +3 -3
  35. package/site/content/docs/recipes/redis.mdx +2 -2
  36. package/site/content/docs/recipes/rustfs.mdx +10 -7
  37. package/site/content/docs/recipes/sglang.mdx +1 -1
  38. package/site/content/docs/recipes/supabase-docker.mdx +1 -1
  39. package/site/content/docs/recipes/timescale.mdx +2 -2
  40. package/site/content/docs/recipes/valkey.mdx +1 -1
  41. package/site/content/docs/recipes/vllm.mdx +1 -1
  42. package/site/content/docs/recipes/yugabytedb.mdx +2 -2
  43. package/site/content/docs/reference/cli.md +61 -38
  44. package/site/content/docs/reference/configuration.mdx +145 -70
  45. package/site/content/docs/reference/environment-variables.mdx +36 -28
  46. package/site/content/docs/reference/errors.mdx +28 -29
  47. package/site/content/docs/reference/fx.mdx +11 -4
  48. package/site/content/docs/reference/i18n.mdx +17 -6
  49. package/site/content/docs/reference/index.mdx +5 -0
  50. package/site/content/docs/reference/meta.json +1 -0
  51. package/site/content/docs/reference/migrating-environments.mdx +158 -0
  52. package/site/content/docs/reference/plugins.mdx +2 -2
  53. package/src/auth/gate-auth.test.ts +12 -10
  54. package/src/cli/ai-setup/ai-setup.test.ts +27 -15
  55. package/src/cli/ai-setup/apply.ts +111 -15
  56. package/src/cli/ai-setup/catalog.ts +3 -3
  57. package/src/cli/ai-setup/index.ts +1 -1
  58. package/src/cli/ai-setup/prompts.ts +1 -1
  59. package/src/cli/ai-setup/recommend.test.ts +1 -1
  60. package/src/cli/ask-seed.test.ts +3 -3
  61. package/src/cli/build.ts +1 -1
  62. package/src/cli/db-auto-push.test.ts +6 -3
  63. package/src/cli/db-auto-push.ts +7 -5
  64. package/src/cli/db-seed.ts +7 -8
  65. package/src/cli/db.test.ts +70 -71
  66. package/src/cli/db.ts +86 -12
  67. package/src/cli/dev-controller.test.ts +35 -0
  68. package/src/cli/dev-controller.ts +298 -0
  69. package/src/cli/dev-controls.ts +11 -65
  70. package/src/cli/dev-db-push.test.ts +47 -15
  71. package/src/cli/dev-schema-sync.test.ts +11 -11
  72. package/src/cli/dev-schema-sync.ts +10 -10
  73. package/src/cli/dev-session-lock.test.ts +106 -0
  74. package/src/cli/dev-session-lock.ts +209 -0
  75. package/src/cli/dev.test.ts +166 -121
  76. package/src/cli/dev.ts +138 -192
  77. package/src/cli/doc-staleness.test.ts +3 -6
  78. package/src/cli/docker-clean.test.ts +33 -1
  79. package/src/cli/docker-clean.ts +28 -33
  80. package/src/cli/docker-cli.test.ts +12 -11
  81. package/src/cli/docker.ts +95 -20
  82. package/src/cli/doctor.test.ts +40 -0
  83. package/src/cli/doctor.ts +55 -15
  84. package/src/cli/drizzle-env.test.ts +11 -13
  85. package/src/cli/drizzle-env.ts +18 -11
  86. package/src/cli/ensure-drizzle-config.ts +11 -14
  87. package/src/cli/hero-meta.test.ts +15 -15
  88. package/src/cli/hero-meta.ts +10 -9
  89. package/src/cli/index.ts +36 -6
  90. package/src/cli/load-config.images.test.ts +16 -12
  91. package/src/cli/load-config.source.test.ts +50 -0
  92. package/src/cli/load-config.ts +34 -28
  93. package/src/cli/meilisearch-local.ts +1 -2
  94. package/src/cli/mode.ts +7 -58
  95. package/src/cli/registry.ts +175 -19
  96. package/src/cli/resolve-dev-sql-env.test.ts +8 -16
  97. package/src/cli/resolve-dev-sql-env.ts +13 -15
  98. package/src/cli/safe-defaults.test.ts +28 -10
  99. package/src/cli/schema.ts +65 -16
  100. package/src/cli/test.ts +58 -0
  101. package/src/cli/tui/App.tsx +252 -0
  102. package/src/cli/tui/DevLive.tsx +116 -0
  103. package/src/cli/tui/components/ConfirmDialog.tsx +48 -0
  104. package/src/cli/tui/components/HeaderBar.tsx +67 -0
  105. package/src/cli/tui/components/LogStream.tsx +47 -0
  106. package/src/cli/tui/components/PanelCard.tsx +39 -0
  107. package/src/cli/tui/components/SelectRow.tsx +37 -0
  108. package/src/cli/tui/components/SlashPalette.tsx +172 -0
  109. package/src/cli/tui/components/Spinner.tsx +32 -0
  110. package/src/cli/tui/components/StatusBar.tsx +51 -0
  111. package/src/cli/tui/components/StatusDot.tsx +37 -0
  112. package/src/cli/tui/components/TabBar.tsx +49 -0
  113. package/src/cli/tui/components/Wordmark.tsx +43 -0
  114. package/src/cli/tui/format.ts +38 -0
  115. package/src/cli/tui/keys.test.ts +18 -0
  116. package/src/cli/tui/keys.ts +26 -0
  117. package/src/cli/tui/launch.test.ts +18 -0
  118. package/src/cli/tui/launch.ts +57 -0
  119. package/src/cli/tui/panels/Dashboard.tsx +201 -0
  120. package/src/cli/tui/panels/Database.tsx +198 -0
  121. package/src/cli/tui/panels/Dev.tsx +257 -0
  122. package/src/cli/tui/panels/Docker.tsx +357 -0
  123. package/src/cli/tui/panels/Navigator.test.ts +22 -0
  124. package/src/cli/tui/panels/Navigator.tsx +213 -0
  125. package/src/cli/tui/slash-catalog.test.ts +31 -0
  126. package/src/cli/tui/slash-catalog.ts +184 -0
  127. package/src/cli/tui/slash-run.ts +54 -0
  128. package/src/cli/tui/state/ports.ts +95 -0
  129. package/src/cli/tui/theme.ts +24 -0
  130. package/src/cli/vault-cmd.test.ts +402 -0
  131. package/src/cli/vault-cmd.ts +727 -69
  132. package/src/cli/vault-secure-input.test.ts +56 -0
  133. package/src/cli/vault-secure-input.ts +124 -0
  134. package/src/compiler/effects-infer.ts +7 -0
  135. package/src/compiler/fixtures/skyport/oke.config.ts +3 -3
  136. package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -1
  137. package/src/compiler/fixtures/skyport.expected.json +1 -1
  138. package/src/compiler-entry.ts +7 -0
  139. package/src/config/define-config.test.ts +111 -47
  140. package/src/config/driver-defaults.test.ts +16 -23
  141. package/src/config/driver-defaults.ts +33 -63
  142. package/src/config/index.ts +169 -144
  143. package/src/config/resolve-driver.test.ts +14 -19
  144. package/src/console/server/app.ts +4 -2
  145. package/src/console/server/console.test.ts +58 -1
  146. package/src/console/server/flows.ts +22 -2
  147. package/src/console/server/index.ts +3 -0
  148. package/src/console/server/operator-db.test.ts +55 -13
  149. package/src/console/server/operator-db.ts +305 -263
  150. package/src/console/server/serve.ts +16 -7
  151. package/src/console/server/state.ts +31 -9
  152. package/src/console/server/vault.test.ts +120 -5
  153. package/src/console/server/vault.ts +208 -12
  154. package/src/console/ui/dist/assets/{index-D-9KFce5.js → index--Y0Uaz-m.js} +2 -2
  155. package/src/console/ui/dist/assets/panel-vault-BkZdJPX8.js +1 -0
  156. package/src/console/ui/dist/assets/style-C3L349iZ.css +3 -0
  157. package/src/console/ui/dist/index.html +2 -2
  158. package/src/console/ui/shell/client.ts +16 -0
  159. package/src/console/ui/shell/panels/vault/VaultPanel.tsx +54 -1
  160. package/src/console/ui/vault/VaultA11yView.tsx +33 -1
  161. package/src/console/ui/vault/backend.test.ts +75 -0
  162. package/src/console/ui/vault/backend.ts +117 -0
  163. package/src/console/ui/vault/fixture.ts +21 -2
  164. package/src/console/ui/vault/index.ts +11 -1
  165. package/src/console/ui/vault/types.ts +26 -0
  166. package/src/docker/compose.ts +164 -61
  167. package/src/docker/derive.ts +106 -12
  168. package/src/docker/docker.test.ts +156 -110
  169. package/src/docker/helpers.ts +98 -0
  170. package/src/docker/images-config.test.ts +3 -8
  171. package/src/docker/index.ts +30 -5
  172. package/src/docker/recipes/index.ts +0 -3
  173. package/src/docker/recipes/llama-cpp.ts +4 -4
  174. package/src/docker/recipes/pgdog.ts +9 -2
  175. package/src/docker/resources.test.ts +71 -0
  176. package/src/docker/resources.ts +176 -0
  177. package/src/docker/stack-id.test.ts +11 -4
  178. package/src/docker/stack-id.ts +42 -32
  179. package/src/docker/stack.integration.test.ts +4 -15
  180. package/src/docker/types.ts +35 -3
  181. package/src/drivers/ai-ollama.integration.test.ts +2 -2
  182. package/src/drivers/conformance.test.ts +11 -12
  183. package/src/drivers/drizzle-dialect.test.ts +0 -4
  184. package/src/drivers/drizzle-dialect.ts +7 -13
  185. package/src/drivers/index.ts +24 -7
  186. package/src/drivers/journal-postgres.test.ts +3 -1
  187. package/src/drivers/journal-postgres.ts +4 -2
  188. package/src/drivers/meilisearch.ts +1 -1
  189. package/src/drivers/pglite.ts +28 -5
  190. package/src/drivers/types.ts +9 -11
  191. package/src/drivers/vault-aws-secrets-manager.test.ts +159 -0
  192. package/src/drivers/vault-aws-secrets-manager.ts +333 -0
  193. package/src/drivers/vault-builtin.test.ts +110 -0
  194. package/src/drivers/vault-builtin.ts +184 -0
  195. package/src/drivers/vault-driver-removal.test.ts +13 -0
  196. package/src/drivers/vault-managed.test.ts +95 -0
  197. package/src/drivers/vault-managed.ts +101 -25
  198. package/src/drivers/vault-types.ts +25 -5
  199. package/src/elements/ai/runtime.ts +1 -1
  200. package/src/elements/clock/chaos.test.ts +4 -1
  201. package/src/elements/clock/durable.ts +57 -4
  202. package/src/elements/clock.test.ts +13 -14
  203. package/src/elements/gate/constants.ts +18 -0
  204. package/src/elements/gate.test.ts +1 -1
  205. package/src/elements/gate.ts +2 -2
  206. package/src/elements/store/domain-ddl.test.ts +5 -12
  207. package/src/elements/store/emit-drizzle.ts +44 -19
  208. package/src/elements/store/index-boot.test.ts +15 -116
  209. package/src/elements/store/load-plugin-tables.ts +1 -1
  210. package/src/elements/store/resource-list-docs.fixture.ts +12 -6
  211. package/src/elements/store/resource-list-docs.test.ts +64 -44
  212. package/src/elements/store/resource.test.ts +17 -10
  213. package/src/elements/store/runtime.ts +9 -12
  214. package/src/elements/store/schema-decl.test.ts +16 -32
  215. package/src/elements/store/seed.test.ts +1 -2
  216. package/src/elements/store/seed.ts +7 -8
  217. package/src/elements/store/sql-condition.test.ts +2 -2
  218. package/src/elements/store/sql-session.test.ts +8 -10
  219. package/src/elements/store/sql-session.ts +10 -2
  220. package/src/elements/store.test.ts +13 -13
  221. package/src/elements/vault/audit.ts +233 -0
  222. package/src/elements/vault/boot-chain.test.ts +78 -0
  223. package/src/elements/vault/boot-chain.ts +43 -39
  224. package/src/elements/vault/builtin-adapter.test.ts +479 -0
  225. package/src/elements/vault/builtin-adapter.ts +1184 -0
  226. package/src/elements/vault/crypto.test.ts +237 -0
  227. package/src/elements/vault/crypto.ts +431 -0
  228. package/src/elements/vault/declare.ts +164 -3
  229. package/src/elements/vault/errors.ts +56 -0
  230. package/src/elements/vault/kms-unseal.test.ts +76 -0
  231. package/src/elements/vault/kms-unseal.ts +181 -0
  232. package/src/elements/vault/path.ts +90 -0
  233. package/src/elements/vault/resilience.test.ts +102 -0
  234. package/src/elements/vault/resilience.ts +107 -0
  235. package/src/elements/vault/runtime.ts +36 -4
  236. package/src/elements/vault/security-checklist.test.ts +130 -0
  237. package/src/elements/vault/storage.ts +448 -0
  238. package/src/elements/vault/types.ts +288 -0
  239. package/src/elements/vault/unseal.ts +150 -0
  240. package/src/elements/vault.test.ts +72 -4
  241. package/src/elements/vault.ts +91 -2
  242. package/src/full.ts +248 -0
  243. package/src/http.ts +41 -0
  244. package/src/i18n/interpolate-simple.ts +37 -0
  245. package/src/i18n/messages.ts +51 -4
  246. package/src/i18n-entry.ts +7 -0
  247. package/src/index.ts +35 -145
  248. package/src/journal-entry.ts +20 -0
  249. package/src/kernel/adopt-barrel-fresh.test.ts +25 -10
  250. package/src/kernel/app.ts +40 -42
  251. package/src/kernel/auto-registry.test.ts +2 -2
  252. package/src/kernel/boot-bind/ai.test.ts +5 -5
  253. package/src/kernel/boot-bind/ai.ts +2 -1
  254. package/src/kernel/boot-bind/channel.test.ts +6 -6
  255. package/src/kernel/boot-bind/channel.ts +3 -10
  256. package/src/kernel/boot-bind/clock.ts +2 -2
  257. package/src/kernel/boot-bind/gate.ts +5 -9
  258. package/src/kernel/boot-bind/honor-config.test.ts +42 -42
  259. package/src/kernel/boot-bind/journal.ts +2 -2
  260. package/src/kernel/boot-bind/store.test.ts +43 -44
  261. package/src/kernel/boot-bind/store.ts +16 -41
  262. package/src/kernel/boot-bind/vault.ts +16 -1
  263. package/src/kernel/boot.test.ts +12 -4
  264. package/src/kernel/boot.ts +31 -19
  265. package/src/kernel/budget-entry.ts +1 -1
  266. package/src/kernel/compensate.test.ts +288 -1
  267. package/src/kernel/compensate.ts +126 -0
  268. package/src/kernel/concurrency.test.ts +10 -9
  269. package/src/kernel/effects-stamping.test.ts +27 -2
  270. package/src/kernel/element-registries.ts +5 -3
  271. package/src/kernel/flow.ts +4 -2
  272. package/src/kernel/fx.test.ts +119 -3
  273. package/src/kernel/fx.ts +215 -40
  274. package/src/kernel/index.ts +14 -0
  275. package/src/kernel/journal.ts +130 -7
  276. package/src/kernel/pipeline.test.ts +20 -6
  277. package/src/kernel/redacted.test.ts +35 -0
  278. package/src/kernel/redacted.ts +24 -2
  279. package/src/kernel/router/create-default.ts +16 -0
  280. package/src/kernel/router/create-edge.ts +18 -0
  281. package/src/kernel/router/create.ts +23 -0
  282. package/src/kernel/router/index.ts +23 -0
  283. package/src/kernel/router/linear.ts +88 -0
  284. package/src/kernel/router/regexp.ts +188 -0
  285. package/src/kernel/router/smart.ts +120 -0
  286. package/src/kernel/router/trie.ts +108 -0
  287. package/src/kernel/router/types.ts +56 -0
  288. package/src/kernel/router.ts +18 -532
  289. package/src/kernel-entry.ts +43 -0
  290. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  291. package/src/manifest/fixtures/skyport.manifest.json +1 -1
  292. package/src/plugins/config-source.test.ts +7 -7
  293. package/src/release/build-lib.ts +58 -0
  294. package/src/release/exports.test.ts +4 -2
  295. package/src/release/exports.ts +30 -4
  296. package/src/release/measure.ts +101 -22
  297. package/src/runs/duckdb.ts +7 -2
  298. package/src/shared/optional-peer.ts +41 -0
  299. package/src/term.test.ts +3 -44
  300. package/src/test/create-test-app.ts +12 -0
  301. package/src/test/provisions.integration.test.ts +1 -1
  302. package/src/test/reset-element-registries.ts +2 -0
  303. package/src/upgrade/codemods.test.ts +7 -20
  304. package/src/upgrade/codemods.ts +93 -60
  305. package/site/content/docs/recipes/openbao.mdx +0 -141
  306. package/src/cli/ask-dev-mode.ts +0 -40
  307. package/src/cli/dev-mode.test.ts +0 -45
  308. package/src/cli/dev-mode.ts +0 -78
  309. package/src/cli/openbao-bootstrap.test.ts +0 -150
  310. package/src/cli/openbao-bootstrap.ts +0 -321
  311. package/src/cli/openbao-restart.integration.test.ts +0 -145
  312. package/src/console/ui/dist/assets/panel-vault-Za1GyJfM.js +0 -1
  313. package/src/console/ui/dist/assets/style-C8MxEWPd.css +0 -3
  314. package/src/docker/recipes/openbao.ts +0 -47
  315. package/src/drivers/libsql.ts +0 -179
  316. package/src/drivers/sqlite.ts +0 -44
  317. package/src/drivers/vault-openbao.test.ts +0 -97
  318. package/src/drivers/vault-openbao.ts +0 -143
@@ -1,19 +1,19 @@
1
1
  /**
2
- * Four compose override layers ending in an untouched `compose.override.yml`.
2
+ * Compose artefact emission three production-grade layouts:
3
3
  *
4
- * 1. `compose.yml` — app + network (generated)
5
- * 2. `compose.<role>.yml` per-role services (generated)
6
- * 3. `compose.prod.yml` prod overlays (generated when `--prod`)
7
- * 4. `compose.override.yml` — user-owned; oke never writes it
4
+ * - `single` (default) → `docker-compose.yml`
5
+ * - `split` → `compose.yml` + `compose.<role>.yml` (+ `compose.prod.yml` when prod)
6
+ * - `stack` → `docker-stack.yml` (Swarm `docker stack deploy -c`)
8
7
  *
9
- * Also emits {@link COMPOSE_ALL} — layers 1–3 deep-merged into one file
10
- * (for Swarm `stack deploy -c` and single-file preferrers). Not part of the
11
- * `-f` merge order; never includes layer 4.
8
+ * User-owned overrides (`docker-compose.override.yml` / `compose.override.yml`)
9
+ * are listed in {@link emitComposeLayers}'s `composeFiles` but never written.
12
10
  */
13
11
 
14
- import { defaultHostPort, envPrefix, serviceNameFor, toYaml } from "./helpers.ts";
12
+ import { composeToYaml, defaultHostPort, envPrefix, serviceNameFor } from "./helpers.ts";
15
13
  import { recipeFor } from "./recipes/index.ts";
14
+ import { allocateServiceResources, mergeDeployResources, type ServerBudget } from "./resources.ts";
16
15
  import type {
16
+ ComposeLayout,
17
17
  DeriveOptions,
18
18
  GeneratedFile,
19
19
  ImageRecipe,
@@ -25,14 +25,26 @@ import { generateCredentials } from "./credentials.ts";
25
25
  import { extraHostPortForInstance, hostPortForInstance, STACK_CONTROL_KEYS } from "./stack-id.ts";
26
26
  import { APP_PORT } from "../runtime/types.ts";
27
27
 
28
- /** Canonical layer-4 filename — never written by derivation. */
28
+ /** Default single-file compose artefact. */
29
+ export const DOCKER_COMPOSE = "docker-compose.yml";
30
+
31
+ /** User-owned override beside {@link DOCKER_COMPOSE} — never written by oke. */
32
+ export const DOCKER_COMPOSE_OVERRIDE = "docker-compose.override.yml";
33
+
34
+ /** Swarm stack artefact (`docker stack deploy -c`). */
35
+ export const DOCKER_STACK = "docker-stack.yml";
36
+
37
+ /** Split-layout base layer. */
38
+ export const COMPOSE_BASE = "compose.yml";
39
+
40
+ /** Split-layout user override — never written by derivation. */
29
41
  export const COMPOSE_OVERRIDE = "compose.override.yml";
30
42
 
31
43
  /**
32
- * Fully merged compose (layers 1–3) additive single-file alternative.
33
- * Does not replace or shadow the base {@code compose.yml} layer.
44
+ * @deprecated Prefer {@link DOCKER_COMPOSE} or {@link DOCKER_STACK}. Kept as an
45
+ * alias of {@link DOCKER_COMPOSE} for soft-compat with older tests / docs.
34
46
  */
35
- export const COMPOSE_ALL = "compose.all.yml";
47
+ export const COMPOSE_ALL = DOCKER_COMPOSE;
36
48
 
37
49
  /**
38
50
  * Relative path refs for compose files living under {@link DeriveOptions.composeDir}.
@@ -80,7 +92,7 @@ export function buildSpecs(options: DeriveOptions): ServiceSpec[] {
80
92
  }
81
93
 
82
94
  /**
83
- * Emit compose layers 1–3 (+ Dockerfile companion handled elsewhere).
95
+ * Emit compose artefacts for the chosen {@link ComposeLayout}.
84
96
  *
85
97
  * @param specs - Normalised services
86
98
  * @param options - Derive options
@@ -93,15 +105,18 @@ export function emitComposeLayers(
93
105
  const appPort = options.appPort ?? APP_PORT;
94
106
  const app = options.app ?? "app";
95
107
  const includeApp = options.includeApp !== false;
108
+ const layout: ComposeLayout = options.layout ?? "single";
96
109
  const paths = composePathRefs(options.composeDir ?? DEFAULT_DOCKER_DIR);
97
110
  const files: GeneratedFile[] = [];
98
- /** Layers 1–3 as objects — same merge Compose would apply via `-f` order. */
111
+ /** Layers as objects — same merge Compose would apply via `-f` order. */
99
112
  const mergeLayers: Record<string, unknown>[] = [];
100
113
 
101
114
  // Layer 1 — project name + network (+ optional app for deploy / oke docker)
102
115
  const base: Record<string, unknown> = {
103
116
  name: `oke-${app}`,
104
- networks: { oke: { driver: "bridge" } },
117
+ networks: {
118
+ oke: { driver: layout === "stack" ? "overlay" : "bridge" },
119
+ },
105
120
  };
106
121
  if (includeApp) {
107
122
  // `image` + `build`: local compose builds and tags; Swarm stack deploy
@@ -113,10 +128,14 @@ export function emitComposeLayers(
113
128
  base.services = {
114
129
  app: {
115
130
  image: `oke-${app}:latest`,
116
- build: {
117
- context: paths.buildContext,
118
- dockerfile: paths.dockerfile,
119
- },
131
+ ...(layout === "stack"
132
+ ? {}
133
+ : {
134
+ build: {
135
+ context: paths.buildContext,
136
+ dockerfile: paths.dockerfile,
137
+ },
138
+ }),
120
139
  ...(hasProxy ? {} : { ports: [`${appPort}:${appPort}`] }),
121
140
  env_file: [paths.envFile],
122
141
  depends_on: Object.fromEntries(
@@ -127,9 +146,9 @@ export function emitComposeLayers(
127
146
  };
128
147
  }
129
148
  mergeLayers.push(base);
130
- files.push({ path: "compose.yml", content: `${toYaml(base)}\n` });
131
149
 
132
150
  // Layer 2 — per-role
151
+ const roleDocs: { readonly role: string; readonly doc: Record<string, unknown> }[] = [];
133
152
  for (const spec of specs) {
134
153
  const recipe = recipeFor(spec.image, recipes);
135
154
  const applied = recipe.apply(spec);
@@ -177,11 +196,11 @@ export function emitComposeLayers(
177
196
  doc.volumes = namedVolumes;
178
197
  }
179
198
  mergeLayers.push(doc);
180
- const path = `compose.${spec.role}.yml`;
181
- files.push({ path, content: `${toYaml(doc)}\n` });
199
+ roleDocs.push({ role: spec.role, doc });
182
200
  }
183
201
 
184
- // Layer 3 — prod overlay (Swarm-aware deploy + app readiness healthcheck)
202
+ // Layer 3 — prod overlay (readiness + deploy policy; resources budgeted below)
203
+ let prodDoc: Record<string, unknown> | undefined;
185
204
  if (options.prod) {
186
205
  const prodServices: Record<string, unknown> = {};
187
206
  if (includeApp) {
@@ -218,9 +237,6 @@ export function emitComposeLayers(
218
237
  max_attempts: 3,
219
238
  window: "120s",
220
239
  },
221
- resources: {
222
- limits: { cpus: "1.0", memory: "512M" },
223
- },
224
240
  },
225
241
  secrets: specs.flatMap((s) => secretNames(s)),
226
242
  };
@@ -228,37 +244,117 @@ export function emitComposeLayers(
228
244
  for (const spec of specs) {
229
245
  prodServices[spec.serviceName] = {
230
246
  deploy: {
231
- resources: {
232
- limits: { cpus: "1.0", memory: "512M" },
247
+ restart_policy: {
248
+ condition: "on-failure",
249
+ delay: "5s",
250
+ max_attempts: 3,
251
+ window: "120s",
233
252
  },
234
253
  },
235
254
  secrets: secretNames(spec),
236
255
  };
237
256
  }
238
- const prodDoc = { services: prodServices };
257
+ prodDoc = { services: prodServices };
239
258
  mergeLayers.push(prodDoc);
240
- files.push({
241
- path: "compose.prod.yml",
242
- content: `${toYaml(prodDoc)}\n`,
243
- });
244
259
  }
245
260
 
246
- // Additive single-file merge of layers 1–3 (not in `-f` order; no layer 4).
247
261
  const merged = mergeLayers.reduce<Record<string, unknown>>(
248
262
  (acc, layer) => deepMergeCompose(acc, layer),
249
263
  {},
250
264
  );
251
- files.push({ path: COMPOSE_ALL, content: `${toYaml(merged)}\n` });
252
-
253
- // Layer 4 — never written. Document merge order only.
254
- const composeFiles = [
255
- "compose.yml",
256
- ...specs.map((s) => `compose.${s.role}.yml`),
257
- ...(options.prod ? ["compose.prod.yml"] : []),
258
- COMPOSE_OVERRIDE,
259
- ];
260
265
 
261
- return { files, composeFiles };
266
+ if (options.prod) {
267
+ applyResourceBudget(merged, {
268
+ cpus: options.serverCpus,
269
+ memoryGb: options.serverMemoryGb,
270
+ });
271
+ }
272
+
273
+ if (layout === "single") {
274
+ files.push({ path: DOCKER_COMPOSE, content: formatComposeYaml(merged) });
275
+ return {
276
+ files,
277
+ composeFiles: [DOCKER_COMPOSE, DOCKER_COMPOSE_OVERRIDE],
278
+ };
279
+ }
280
+
281
+ if (layout === "stack") {
282
+ files.push({ path: DOCKER_STACK, content: formatComposeYaml(merged) });
283
+ return {
284
+ files,
285
+ composeFiles: [DOCKER_STACK],
286
+ };
287
+ }
288
+
289
+ // split — layered files + optional prod overlay; override never written
290
+ files.push({ path: COMPOSE_BASE, content: formatComposeYaml(base) });
291
+ for (const { role, doc } of roleDocs) {
292
+ files.push({ path: `compose.${role}.yml`, content: formatComposeYaml(doc) });
293
+ }
294
+ if (prodDoc) {
295
+ // Re-emit prod overlay with budgeted resources so `-f` merge matches single.
296
+ const budgetedProd = budgetProdOverlay(prodDoc, merged);
297
+ files.push({
298
+ path: "compose.prod.yml",
299
+ content: formatComposeYaml(budgetedProd),
300
+ });
301
+ }
302
+ return {
303
+ files,
304
+ composeFiles: [
305
+ COMPOSE_BASE,
306
+ ...specs.map((s) => `compose.${s.role}.yml`),
307
+ ...(prodDoc ? ["compose.prod.yml"] : []),
308
+ COMPOSE_OVERRIDE,
309
+ ],
310
+ };
311
+ }
312
+
313
+ /**
314
+ * Write budgeted `deploy.resources` onto every service in a merged compose doc.
315
+ *
316
+ * @param doc - Merged compose document (mutated)
317
+ * @param budget - Host capacity
318
+ */
319
+ function applyResourceBudget(doc: Record<string, unknown>, budget: Partial<ServerBudget>): void {
320
+ const services = doc.services;
321
+ if (!isPlainObject(services)) return;
322
+ const names = Object.keys(services);
323
+ const limits = allocateServiceResources(names, budget);
324
+ for (const name of names) {
325
+ const limit = limits.get(name);
326
+ if (!limit) continue;
327
+ const svc = services[name];
328
+ if (!isPlainObject(svc)) continue;
329
+ const existingDeploy = isPlainObject(svc.deploy) ? svc.deploy : undefined;
330
+ svc.deploy = mergeDeployResources(limit, existingDeploy);
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Copy budgeted `deploy.resources` from the merged doc onto the prod overlay
336
+ * so split `-f` merges stay aligned with the single-file output.
337
+ *
338
+ * @param prodDoc - Prod overlay before budgeting
339
+ * @param merged - Fully merged (already budgeted) document
340
+ */
341
+ function budgetProdOverlay(
342
+ prodDoc: Record<string, unknown>,
343
+ merged: Record<string, unknown>,
344
+ ): Record<string, unknown> {
345
+ const out = deepMergeCompose({}, prodDoc);
346
+ const outServices = out.services;
347
+ const mergedServices = merged.services;
348
+ if (!isPlainObject(outServices) || !isPlainObject(mergedServices)) return out;
349
+ for (const [name, svc] of Object.entries(outServices)) {
350
+ if (!isPlainObject(svc)) continue;
351
+ const mergedSvc = mergedServices[name];
352
+ if (!isPlainObject(mergedSvc) || !isPlainObject(mergedSvc.deploy)) continue;
353
+ const mergedDeploy = mergedSvc.deploy;
354
+ const prevDeploy = isPlainObject(svc.deploy) ? svc.deploy : {};
355
+ svc.deploy = deepMergeCompose({ ...prevDeploy }, { ...mergedDeploy });
356
+ }
357
+ return out;
262
358
  }
263
359
 
264
360
  /**
@@ -359,15 +455,6 @@ export function buildStackEnv(
359
455
  env.SMTP_HOST = host;
360
456
  env.SMTP_PORT = String(spec.hostPort);
361
457
  if (uiHost !== undefined) env.MAILPIT_UI_URL = `http://${host}:${uiHost}`;
362
- } else if (spec.role === "vault") {
363
- // Token is minted by OpenBao bootstrap and preserved via stack controls —
364
- // never a generated password here.
365
- env[`${prefix}_URL`] = url;
366
- env.OKE_VAULT_URL = url;
367
- if (controls.OKE_VAULT_TOKEN) {
368
- env.OKE_VAULT_TOKEN = controls.OKE_VAULT_TOKEN;
369
- env.OKE_VAULT_MOUNT = controls.OKE_VAULT_MOUNT ?? "secret";
370
- }
371
458
  } else if (spec.role === "store.index") {
372
459
  // Meilisearch: standalone HTTP URL + the generated master key.
373
460
  env[`${prefix}_URL`] = url;
@@ -419,11 +506,31 @@ const ROLE_SECTION_TITLE: Readonly<Record<string, string>> = {
419
506
  "store.index": "store.index — search index",
420
507
  "channel.email": "channel.email — Mailpit (SMTP + UI)",
421
508
  signal: "signal — message bus",
422
- vault: "vault — OpenBao",
423
509
  ai: "ai — local inference (llama.cpp / Ollama / vLLM / SGLang)",
424
510
  proxy: "proxy — TLS terminator (Caddy / Traefik)",
425
511
  };
426
512
 
513
+ /**
514
+ * Serialise a compose mapping with section gaps and per-service comments.
515
+ *
516
+ * @param doc - Merged or layered compose document
517
+ */
518
+ function formatComposeYaml(doc: Record<string, unknown>): string {
519
+ return composeToYaml(doc, { serviceComment: commentForComposeService });
520
+ }
521
+
522
+ /**
523
+ * Human-readable comment for a compose service key.
524
+ *
525
+ * @param serviceName - Compose service name (`store-sql`, `app`, …)
526
+ */
527
+ function commentForComposeService(serviceName: string): string {
528
+ if (serviceName === "app") return "App — okengine runtime";
529
+ if (serviceName === "socket-proxy") return "Docker socket proxy (Traefik companion)";
530
+ const role = serviceName.replaceAll("-", ".");
531
+ return ROLE_SECTION_TITLE[role] ?? role;
532
+ }
533
+
427
534
  /** Friendly aliases emitted beside their role block. */
428
535
  const ROLE_ALIASES: Readonly<Record<string, readonly string[]>> = {
429
536
  "store.sql": ["DATABASE_URL", "PGDATA", "POSTGRES_INITDB_ARGS"],
@@ -454,7 +561,6 @@ const ROLE_ALIASES: Readonly<Record<string, readonly string[]>> = {
454
561
  // OKE_AI_URL is already emitted as `${prefix}_URL` — do not alias it again.
455
562
  ai: ["OKE_AI_MODEL", "OKE_AI_CTX_SIZE"],
456
563
  proxy: ["OKE_PROXY_HOST", "OKE_PROXY_ACME_EMAIL"],
457
- vault: ["OKE_VAULT_TOKEN", "OKE_VAULT_MOUNT"],
458
564
  "store.index": ["OKE_STORE_INDEX_KEY"],
459
565
  };
460
566
 
@@ -474,7 +580,7 @@ const ROLE_CONTROL_EXAMPLES: Readonly<Record<string, readonly string[]>> = {
474
580
  // OKE_AI_CTX_SIZE bounds the KV cache llama-server allocates at load —
475
581
  // left at the model's full native context (often 32K-256K+), a small
476
582
  // model's KV cache alone can OOM the container regardless of host RAM.
477
- ai: ["OKE_AI_MODEL=smollm2", "OKE_AI_CTX_SIZE=4096"],
583
+ ai: ["OKE_AI_MODEL=granite3.3:2b", "OKE_AI_CTX_SIZE=4096"],
478
584
  proxy: ["OKE_PROXY_HOST=localhost", "OKE_PROXY_ACME_EMAIL=admin@example.com"],
479
585
  };
480
586
 
@@ -503,9 +609,6 @@ function roleFromEnvKey(key: string): string | undefined {
503
609
  if (key === "OKE_PROXY_URL" || key === "OKE_PROXY_HOST" || key === "OKE_PROXY_ACME_EMAIL") {
504
610
  return "proxy";
505
611
  }
506
- if (key === "OKE_VAULT_TOKEN" || key === "OKE_VAULT_MOUNT" || key === "OKE_VAULT_URL") {
507
- return "vault";
508
- }
509
612
  if (key === "OKE_STORE_INDEX_KEY" || key === "OKE_STORE_INDEX_URL") return "store.index";
510
613
  return undefined;
511
614
  }
@@ -530,7 +633,7 @@ export function formatStackEnv(env: Readonly<Record<string, string>>): string {
530
633
  "# Vault resolution: process.env → .env.local → docker/.env.docker → driver",
531
634
  "#",
532
635
  "# Re-run `oke dev --docker` to regenerate. Put compose tweaks in",
533
- "# compose.override.yml — never put secrets in YAML.",
636
+ "# docker-compose.override.yml (or compose.override.yml for --split) — never put secrets in YAML.",
534
637
  "",
535
638
  ];
536
639
 
@@ -2,11 +2,14 @@
2
2
  * Derive Dockerfile + compose files from config image pins.
3
3
  */
4
4
 
5
- import { mkdirSync } from "node:fs";
5
+ import { mkdirSync, readdirSync, rmSync, statSync, unlinkSync } from "node:fs";
6
+ import { dirname, join } from "node:path";
6
7
  import {
7
8
  assertNoCredentialsInYaml,
8
9
  buildSpecs,
9
10
  buildStackEnv,
11
+ COMPOSE_OVERRIDE,
12
+ DOCKER_COMPOSE_OVERRIDE,
10
13
  emitComposeLayers,
11
14
  formatStackEnv,
12
15
  } from "./compose.ts";
@@ -17,7 +20,7 @@ import {
17
20
  LLAMA_CPP_ENTRYPOINT_FILE,
18
21
  llamaCpp,
19
22
  } from "./recipes/llama-cpp.ts";
20
- import { buildPgDogToml, buildPgDogUsersToml } from "./recipes/pgdog.ts";
23
+ import { buildPgDogToml, buildPgDogUsersToml, PGDOG_CONFIG_DIR } from "./recipes/pgdog.ts";
21
24
  import type { DeriveOptions, DeriveResult, GeneratedFile } from "./types.ts";
22
25
  import { DEFAULT_DOCKER_DIR } from "./types.ts";
23
26
  import { APP_PORT } from "../runtime/types.ts";
@@ -26,10 +29,10 @@ import { APP_PORT } from "../runtime/types.ts";
26
29
  * Derive infrastructure files from normalised image pins.
27
30
  *
28
31
  * Credentials land only in the returned `stackEnv` (for `.env.docker`) and
29
- * in `users.toml` when PgDog is present — never in generated YAML. Layer 4
30
- * (`compose.override.yml`) is listed in `composeFiles` but never written.
32
+ * in `pgdog/users.toml` when PgDog is present — never in generated YAML.
33
+ * User-owned overrides are listed in `composeFiles` but never written.
31
34
  *
32
- * @param options - Images / app / prod flag
35
+ * @param options - Images / app / prod / layout flags
33
36
  */
34
37
  export function deriveInfrastructure(options: DeriveOptions): DeriveResult {
35
38
  if (!options.images || Object.keys(options.images).length === 0) {
@@ -42,6 +45,7 @@ export function deriveInfrastructure(options: DeriveOptions): DeriveResult {
42
45
  ...options,
43
46
  composeDir: options.composeDir ?? DEFAULT_DOCKER_DIR,
44
47
  includeApp: options.includeApp !== false,
48
+ layout: options.layout ?? "single",
45
49
  };
46
50
 
47
51
  const specs = buildSpecs(normalised);
@@ -99,8 +103,8 @@ function llamaCppEntrypointFiles(specs: DeriveResult["specs"]): GeneratedFile[]
99
103
  /**
100
104
  * Emit PgDog TOML configs when both `pgdog` and `store.sql` are in the stack.
101
105
  *
102
- * `pgdog.toml` has no secrets; `users.toml` mirrors store.sql credentials
103
- * (same trust boundary as `.env.docker` — do not commit).
106
+ * `pgdog/pgdog.toml` has no secrets; `pgdog/users.toml` mirrors store.sql
107
+ * credentials (same trust boundary as `.env.docker` — do not commit).
104
108
  *
105
109
  * @param specs - Normalised services
106
110
  */
@@ -110,14 +114,14 @@ function pgdogConfigFiles(specs: DeriveResult["specs"]): GeneratedFile[] {
110
114
  if (!sql || !pooler) return [];
111
115
  return [
112
116
  {
113
- path: "pgdog.toml",
117
+ path: `${PGDOG_CONFIG_DIR}/pgdog.toml`,
114
118
  content: buildPgDogToml({
115
119
  database: sql.credentials.database,
116
120
  postgresPort: sql.port,
117
121
  }),
118
122
  },
119
123
  {
120
- path: "users.toml",
124
+ path: `${PGDOG_CONFIG_DIR}/users.toml`,
121
125
  content: buildPgDogUsersToml({
122
126
  user: sql.credentials.user,
123
127
  password: sql.credentials.password,
@@ -149,8 +153,9 @@ function proxyConfigFiles(
149
153
  }
150
154
 
151
155
  /**
152
- * Write derived files to disk. Never writes `compose.override.yml` or
153
- * credential values into YAML. Optionally writes `docker/.env.docker`.
156
+ * Write derived files to disk. Never writes user overrides or credential
157
+ * values into YAML. Prunes stale generated compose / PgDog artefacts from a
158
+ * previous layout. Optionally writes `docker/.env.docker`.
154
159
  *
155
160
  * @param result - Derive result
156
161
  * @param outDir - Destination for Dockerfile / compose (usually `docker/`)
@@ -166,8 +171,11 @@ export async function writeDerivedFiles(
166
171
  const written: string[] = [];
167
172
  const root = outDir.replace(/\/$/, "");
168
173
  mkdirSync(root, { recursive: true });
174
+ const keep = new Set(result.files.map((f) => f.path));
175
+ pruneStaleGenerated(root, keep);
169
176
  for (const file of result.files) {
170
- const path = `${root}/${file.path}`;
177
+ const path = join(root, file.path);
178
+ mkdirSync(dirname(path), { recursive: true });
171
179
  await Bun.write(path, file.content);
172
180
  written.push(path);
173
181
  }
@@ -178,3 +186,89 @@ export async function writeDerivedFiles(
178
186
  }
179
187
  return written;
180
188
  }
189
+
190
+ /** Generated compose / companion paths safe to delete when absent from a derive. */
191
+ const PRUNE_ROOT_FILES = new Set([
192
+ "Dockerfile",
193
+ "docker-compose.yml",
194
+ "docker-stack.yml",
195
+ "compose.yml",
196
+ "compose.all.yml",
197
+ "compose.prod.yml",
198
+ "Caddyfile",
199
+ LLAMA_CPP_ENTRYPOINT_FILE,
200
+ "pgdog.toml",
201
+ "users.toml",
202
+ ]);
203
+
204
+ /**
205
+ * Remove previously generated artefacts that the current layout no longer emits.
206
+ * Never touches user overrides or `.env.docker`.
207
+ *
208
+ * @param root - Compose directory
209
+ * @param keep - Relative paths that will be rewritten
210
+ */
211
+ function pruneStaleGenerated(root: string, keep: ReadonlySet<string>): void {
212
+ let entries: string[];
213
+ try {
214
+ entries = readdirSync(root);
215
+ } catch {
216
+ return;
217
+ }
218
+ for (const name of entries) {
219
+ if (name === COMPOSE_OVERRIDE || name === DOCKER_COMPOSE_OVERRIDE) continue;
220
+ if (name === ".env.docker") continue;
221
+ const abs = join(root, name);
222
+ let st: ReturnType<typeof statSync>;
223
+ try {
224
+ st = statSync(abs);
225
+ } catch {
226
+ continue;
227
+ }
228
+ if (st.isDirectory()) {
229
+ if (name === PGDOG_CONFIG_DIR) {
230
+ prunePgDogDir(abs, keep);
231
+ }
232
+ continue;
233
+ }
234
+ if (!st.isFile()) continue;
235
+ const rel = name;
236
+ if (keep.has(rel)) continue;
237
+ if (PRUNE_ROOT_FILES.has(name) || /^compose\..+\.yml$/.test(name)) {
238
+ try {
239
+ unlinkSync(abs);
240
+ } catch {
241
+ // best-effort
242
+ }
243
+ }
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Drop legacy / unused files under `pgdog/`.
249
+ *
250
+ * @param dir - Absolute `pgdog/` path
251
+ * @param keep - Relative keep set (`pgdog/…`)
252
+ */
253
+ function prunePgDogDir(dir: string, keep: ReadonlySet<string>): void {
254
+ let entries: string[];
255
+ try {
256
+ entries = readdirSync(dir);
257
+ } catch {
258
+ return;
259
+ }
260
+ for (const name of entries) {
261
+ const rel = `${PGDOG_CONFIG_DIR}/${name}`;
262
+ if (keep.has(rel)) continue;
263
+ try {
264
+ unlinkSync(join(dir, name));
265
+ } catch {
266
+ // best-effort
267
+ }
268
+ }
269
+ try {
270
+ if (readdirSync(dir).length === 0) rmSync(dir, { recursive: true, force: true });
271
+ } catch {
272
+ // best-effort
273
+ }
274
+ }