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
@@ -3,12 +3,14 @@
3
3
  *
4
4
  * Recipes translate a normalised {@link ServiceSpec} into image-specific
5
5
  * env / command / healthcheck and expose {@link ImageRecipe.url} so the
6
- * kernel never learns an env-var name. Four compose override layers end
7
- * in an untouched `compose.override.yml`. Credentials never enter YAML.
6
+ * kernel never learns an env-var name. Default layout is a single
7
+ * production-grade `docker-compose.yml` (opt-in `--split` / `--stack`).
8
+ * Credentials never enter YAML.
8
9
  */
9
10
 
10
11
  export type {
11
12
  ComposeHealthcheck,
13
+ ComposeLayout,
12
14
  DeriveOptions,
13
15
  DeriveResult,
14
16
  GeneratedFile,
@@ -26,7 +28,11 @@ export { DEFAULT_DOCKER_DIR } from "./types.ts";
26
28
 
27
29
  export {
28
30
  COMPOSE_ALL,
31
+ COMPOSE_BASE,
29
32
  COMPOSE_OVERRIDE,
33
+ DOCKER_COMPOSE,
34
+ DOCKER_COMPOSE_OVERRIDE,
35
+ DOCKER_STACK,
30
36
  assertNoCredentialsInYaml,
31
37
  buildSpecs,
32
38
  buildStackEnv,
@@ -36,9 +42,24 @@ export {
36
42
  formatStackEnv,
37
43
  } from "./compose.ts";
38
44
 
45
+ export {
46
+ DEFAULT_SERVER_BUDGET,
47
+ allocateServiceResources,
48
+ mergeDeployResources,
49
+ resolveServerBudget,
50
+ type ServerBudget,
51
+ type ServiceResourceLimit,
52
+ } from "./resources.ts";
53
+
39
54
  export { deriveInfrastructure, writeDerivedFiles } from "./derive.ts";
40
55
  export { emitDockerfile } from "./dockerfile.ts";
41
- export { credEnv, envPrefix, serviceNameFor } from "./helpers.ts";
56
+ export {
57
+ composeToYaml,
58
+ credEnv,
59
+ envPrefix,
60
+ serviceNameFor,
61
+ type ComposeYamlOptions,
62
+ } from "./helpers.ts";
42
63
  export { generateCredentials } from "./credentials.ts";
43
64
  export {
44
65
  ensureOllamaModel,
@@ -92,7 +113,6 @@ export {
92
113
  ollama,
93
114
  OLLAMA_IMAGE,
94
115
  OLLAMA_MIN_SAFE_VERSION,
95
- openbao,
96
116
  pgdog,
97
117
  postgres,
98
118
  recipeFor,
@@ -108,7 +128,12 @@ export {
108
128
  yugabyte,
109
129
  } from "./recipes/index.ts";
110
130
  export { CADDY_APP_SERVICE, buildCaddyfile } from "./recipes/caddy.ts";
111
- export { PGDOG_BACKEND_SERVICE, buildPgDogToml, buildPgDogUsersToml } from "./recipes/pgdog.ts";
131
+ export {
132
+ PGDOG_BACKEND_SERVICE,
133
+ PGDOG_CONFIG_DIR,
134
+ buildPgDogToml,
135
+ buildPgDogUsersToml,
136
+ } from "./recipes/pgdog.ts";
112
137
  export { SOCKET_PROXY_IMAGE, SOCKET_PROXY_SERVICE, traefikAppLabels } from "./recipes/traefik.ts";
113
138
  export {
114
139
  formatStackPreview,
@@ -10,7 +10,6 @@ import { llamaCpp } from "./llama-cpp.ts";
10
10
  import { mailpit } from "./mailpit.ts";
11
11
  import { meilisearch } from "./meilisearch.ts";
12
12
  import { ollama } from "./ollama.ts";
13
- import { openbao } from "./openbao.ts";
14
13
  import { pgdog } from "./pgdog.ts";
15
14
  import { postgres } from "./postgres.ts";
16
15
  import { redis } from "./redis.ts";
@@ -36,7 +35,6 @@ export const builtinRecipes: readonly ImageRecipe[] = [
36
35
  redis,
37
36
  mailpit,
38
37
  rustfs,
39
- openbao,
40
38
  meilisearch,
41
39
  llamaCpp,
42
40
  vllm,
@@ -54,7 +52,6 @@ export {
54
52
  mailpit,
55
53
  meilisearch,
56
54
  ollama,
57
- openbao,
58
55
  pgdog,
59
56
  postgres,
60
57
  redis,
@@ -55,7 +55,7 @@ def cache_dir() -> str:
55
55
 
56
56
 
57
57
  def parse_model(model: str) -> tuple[str, str]:
58
- raw = model.strip() or "smollm2"
58
+ raw = model.strip() or "granite3.3:2b"
59
59
  if "/" not in raw.split(":", 1)[0]:
60
60
  raw = f"ai/{raw}"
61
61
  if ":" in raw:
@@ -187,7 +187,7 @@ def hub_pull(repo: str, tag: str, dest: str) -> None:
187
187
  def ensure_model(model: str) -> str:
188
188
  repo, tag = parse_model(model)
189
189
  # llama download -dr expects id without forced ai/ prefix when using default org.
190
- dr = model.strip() or "smollm2"
190
+ dr = model.strip() or "granite3.3:2b"
191
191
  if dr.startswith("ai/"):
192
192
  dr = dr[3:]
193
193
  dest = os.path.join(cache_dir(), expected_gguf(repo, tag))
@@ -219,7 +219,7 @@ def ensure_model(model: str) -> str:
219
219
 
220
220
 
221
221
  def main() -> None:
222
- model = (os.environ.get("OKE_AI_MODEL") or "smollm2").strip() or "smollm2"
222
+ model = (os.environ.get("OKE_AI_MODEL") or "granite3.3:2b").strip() or "granite3.3:2b"
223
223
  gguf = ensure_model(model)
224
224
  # Without an explicit --ctx-size, llama-server defaults to the model's
225
225
  # full native training context (32K-256K+ for many current models). The
@@ -259,7 +259,7 @@ export const llamaCpp: ImageRecipe = {
259
259
  match: (i) => /llama\.cpp|llamacpp/i.test(i),
260
260
  apply: (s) => ({
261
261
  // OKE_AI_MODEL comes from compose `env_file` (`.env.docker`). Do not
262
- // re-declare it as `${OKE_AI_MODEL:-smollm2}` here — Compose interpolates
262
+ // re-declare it as `${OKE_AI_MODEL:-granite3.3:2b}` here — Compose interpolates
263
263
  // that from the *host* shell and can override the stack file with the
264
264
  // default. Never set LLAMA_ARG_MODELS_PRESET / LLAMA_ARG_DOCKER_REPO
265
265
  // (router children inherit `LLAMA_ARG_*` and can OOM).
@@ -3,10 +3,14 @@
3
3
  *
4
4
  * Sits in front of the `postgres` recipe. Transaction pooling is set
5
5
  * explicitly (`pooler_mode = "transaction"` — also PgDog's upstream default).
6
- * Config shape matches upstream docs: `pgdog.toml` + `users.toml`
6
+ * Config shape matches upstream docs under `pgdog/`:
7
+ * `pgdog/pgdog.toml` + `pgdog/users.toml`
7
8
  * (https://docs.pgdog.dev/configuration/).
8
9
  */
9
10
 
11
+ /** Relative directory for generated PgDog TOML (under the compose dir). */
12
+ export const PGDOG_CONFIG_DIR = "pgdog";
13
+
10
14
  import type { ImageRecipe } from "../types.ts";
11
15
 
12
16
  /** Backend Postgres service name in the compose network. */
@@ -66,7 +70,10 @@ export const pgdog: ImageRecipe = {
66
70
  port: 6432,
67
71
  match: (i) => /pgdog/i.test(i),
68
72
  apply: () => ({
69
- volumes: ["./pgdog.toml:/pgdog/pgdog.toml:ro", "./users.toml:/pgdog/users.toml:ro"],
73
+ volumes: [
74
+ `./${PGDOG_CONFIG_DIR}/pgdog.toml:/pgdog/pgdog.toml:ro`,
75
+ `./${PGDOG_CONFIG_DIR}/users.toml:/pgdog/users.toml:ro`,
76
+ ],
70
77
  dependsOn: {
71
78
  [PGDOG_BACKEND_SERVICE]: { condition: "service_healthy" },
72
79
  },
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Server budget → per-service resource allocation.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import {
7
+ DEFAULT_SERVER_BUDGET,
8
+ allocateServiceResources,
9
+ mergeDeployResources,
10
+ resolveServerBudget,
11
+ } from "./resources.ts";
12
+
13
+ describe("resolveServerBudget", () => {
14
+ test("defaults to 4 CPU / 8 GiB", () => {
15
+ expect(resolveServerBudget(undefined)).toEqual(DEFAULT_SERVER_BUDGET);
16
+ expect(DEFAULT_SERVER_BUDGET).toEqual({ cpus: 4, memoryGb: 8 });
17
+ });
18
+
19
+ test("accepts client overrides", () => {
20
+ expect(resolveServerBudget({ cpus: 8, memoryGb: 16 })).toEqual({
21
+ cpus: 8,
22
+ memoryGb: 16,
23
+ });
24
+ });
25
+ });
26
+
27
+ describe("allocateServiceResources", () => {
28
+ test("apportion 4/8 across app + sql + kv without exceeding the usable pool", () => {
29
+ const limits = allocateServiceResources(["app", "store-sql", "store-kv"], {
30
+ cpus: 4,
31
+ memoryGb: 8,
32
+ });
33
+ expect(limits.size).toBe(3);
34
+ let cpuSum = 0;
35
+ let memSum = 0;
36
+ for (const limit of limits.values()) {
37
+ cpuSum += Number(limit.cpus);
38
+ memSum += limit.memory.endsWith("G")
39
+ ? Number(limit.memory.slice(0, -1)) * 1024
40
+ : Number(limit.memory.slice(0, -1));
41
+ }
42
+ // 10% host overhead → usable 3.6 CPU / ~7372 MiB
43
+ expect(cpuSum).toBeLessThanOrEqual(3.6 + 0.05);
44
+ expect(memSum).toBeLessThanOrEqual(8 * 1024 * 0.9 + 8);
45
+ expect(Number(limits.get("store-sql")!.cpus)).toBeGreaterThan(
46
+ Number(limits.get("store-kv")!.cpus),
47
+ );
48
+ });
49
+
50
+ test("mergeDeployResources preserves GPU reservations", () => {
51
+ const merged = mergeDeployResources(
52
+ { cpus: "1.5", memory: "2G" },
53
+ {
54
+ resources: {
55
+ reservations: {
56
+ devices: [{ driver: "nvidia", count: 1, capabilities: ["gpu"] }],
57
+ },
58
+ },
59
+ },
60
+ );
61
+ const resources = merged.resources as {
62
+ limits: { cpus: string; memory: string };
63
+ reservations: { devices: unknown[]; cpus: string; memory: string };
64
+ };
65
+ expect(resources.limits).toEqual({ cpus: "1.5", memory: "2G" });
66
+ expect(resources.reservations.devices).toEqual([
67
+ { driver: "nvidia", count: 1, capabilities: ["gpu"] },
68
+ ]);
69
+ expect(resources.reservations.cpus).toBe("0.75");
70
+ });
71
+ });
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Server resource budget → per-service Compose `deploy.resources` limits.
3
+ *
4
+ * Defaults assume a small production host (4 CPU / 8 GiB). Callers may override
5
+ * via {@link ServerBudget}. ~10% stays unallocated for the Docker daemon / OS.
6
+ */
7
+
8
+ /** Host capacity used when apportioning container limits. */
9
+ export type ServerBudget = {
10
+ /** Logical CPUs available to the stack (default 4). */
11
+ readonly cpus: number;
12
+ /** RAM in GiB available to the stack (default 8). */
13
+ readonly memoryGb: number;
14
+ };
15
+
16
+ /** Default production host size when the client does not specify one. */
17
+ export const DEFAULT_SERVER_BUDGET: ServerBudget = {
18
+ cpus: 4,
19
+ memoryGb: 8,
20
+ };
21
+
22
+ /** Fraction of the host reserved for Docker / OS (not given to services). */
23
+ const HOST_OVERHEAD = 0.1;
24
+
25
+ /** Relative weights by compose role — heavier roles get a larger share. */
26
+ const ROLE_WEIGHT: Readonly<Record<string, number>> = {
27
+ app: 20,
28
+ "store.sql": 30,
29
+ pgdog: 5,
30
+ "store.kv": 15,
31
+ "store.files": 10,
32
+ "store.index": 10,
33
+ "channel.email": 5,
34
+ ai: 40,
35
+ proxy: 5,
36
+ };
37
+
38
+ const DEFAULT_WEIGHT = 10;
39
+ const MIN_CPUS = 0.1;
40
+ const MIN_MEMORY_MB = 64;
41
+
42
+ /** Resolved CPU + memory limit for one compose service. */
43
+ export type ServiceResourceLimit = {
44
+ readonly cpus: string;
45
+ readonly memory: string;
46
+ };
47
+
48
+ /**
49
+ * Normalise a partial budget against {@link DEFAULT_SERVER_BUDGET}.
50
+ *
51
+ * @param budget - Optional caller overrides
52
+ */
53
+ export function resolveServerBudget(budget: Partial<ServerBudget> | undefined): ServerBudget {
54
+ const cpus = budget?.cpus ?? DEFAULT_SERVER_BUDGET.cpus;
55
+ const memoryGb = budget?.memoryGb ?? DEFAULT_SERVER_BUDGET.memoryGb;
56
+ if (!(cpus > 0) || !Number.isFinite(cpus)) {
57
+ throw new Error(
58
+ `oke docker: server cpus must be a positive number (got ${String(budget?.cpus)})`,
59
+ );
60
+ }
61
+ if (!(memoryGb > 0) || !Number.isFinite(memoryGb)) {
62
+ throw new Error(
63
+ `oke docker: server memoryGb must be a positive number (got ${String(budget?.memoryGb)})`,
64
+ );
65
+ }
66
+ return { cpus, memoryGb };
67
+ }
68
+
69
+ /**
70
+ * Apportion a server budget across named services (compose service names).
71
+ *
72
+ * @param serviceNames - Services that receive limits (`app`, `store-sql`, …)
73
+ * @param budget - Host capacity
74
+ * @returns Map of service name → deploy.resources.limits fields
75
+ */
76
+ export function allocateServiceResources(
77
+ serviceNames: readonly string[],
78
+ budget: Partial<ServerBudget> | undefined = undefined,
79
+ ): ReadonlyMap<string, ServiceResourceLimit> {
80
+ const resolved = resolveServerBudget(budget);
81
+ const usableCpus = resolved.cpus * (1 - HOST_OVERHEAD);
82
+ const usableMemoryMb = resolved.memoryGb * 1024 * (1 - HOST_OVERHEAD);
83
+
84
+ const weights = serviceNames.map((name) => {
85
+ const role = serviceNameToRole(name);
86
+ return ROLE_WEIGHT[role] ?? DEFAULT_WEIGHT;
87
+ });
88
+ const total = weights.reduce((a, b) => a + b, 0);
89
+ const out = new Map<string, ServiceResourceLimit>();
90
+ if (total <= 0 || serviceNames.length === 0) return out;
91
+
92
+ for (let i = 0; i < serviceNames.length; i++) {
93
+ const name = serviceNames[i]!;
94
+ const share = weights[i]! / total;
95
+ const cpus = Math.max(MIN_CPUS, roundCpus(usableCpus * share));
96
+ const memoryMb = Math.max(MIN_MEMORY_MB, Math.round(usableMemoryMb * share));
97
+ out.set(name, {
98
+ cpus: formatCpus(cpus),
99
+ memory: formatMemoryMb(memoryMb),
100
+ });
101
+ }
102
+ return out;
103
+ }
104
+
105
+ /**
106
+ * Build a Compose `deploy.resources` block from a limit, preserving any
107
+ * existing reservations (e.g. GPU devices from AI recipes).
108
+ *
109
+ * @param limit - Allocated CPU / memory
110
+ * @param existing - Prior `deploy` object from a recipe, if any
111
+ */
112
+ export function mergeDeployResources(
113
+ limit: ServiceResourceLimit,
114
+ existing: Readonly<Record<string, unknown>> | undefined,
115
+ ): Record<string, unknown> {
116
+ const prev = existing ? { ...existing } : {};
117
+ const prevResources = isPlainObject(prev.resources)
118
+ ? { ...prev.resources }
119
+ : ({} as Record<string, unknown>);
120
+ const prevLimits = isPlainObject(prevResources.limits)
121
+ ? { ...prevResources.limits }
122
+ : ({} as Record<string, unknown>);
123
+ prevResources.limits = {
124
+ ...prevLimits,
125
+ cpus: limit.cpus,
126
+ memory: limit.memory,
127
+ };
128
+ // Soft reservation ≈ half the limit so the scheduler can pack peers.
129
+ const prevReservations = isPlainObject(prevResources.reservations)
130
+ ? { ...prevResources.reservations }
131
+ : ({} as Record<string, unknown>);
132
+ if (prevReservations.cpus === undefined) {
133
+ prevReservations.cpus = formatCpus(Math.max(MIN_CPUS, Number(limit.cpus) * 0.5));
134
+ }
135
+ if (prevReservations.memory === undefined) {
136
+ prevReservations.memory = formatMemoryMb(
137
+ Math.max(MIN_MEMORY_MB, Math.round(parseMemoryMb(limit.memory) * 0.5)),
138
+ );
139
+ }
140
+ prevResources.reservations = prevReservations;
141
+ prev.resources = prevResources;
142
+ return prev;
143
+ }
144
+
145
+ function serviceNameToRole(serviceName: string): string {
146
+ if (serviceName === "app") return "app";
147
+ return serviceName.replaceAll("-", ".");
148
+ }
149
+
150
+ function roundCpus(n: number): number {
151
+ return Math.round(n * 100) / 100;
152
+ }
153
+
154
+ function formatCpus(n: number): string {
155
+ return String(Math.round(n * 100) / 100);
156
+ }
157
+
158
+ function formatMemoryMb(mb: number): string {
159
+ if (mb >= 1024 && mb % 1024 === 0) return `${mb / 1024}G`;
160
+ if (mb >= 1024) {
161
+ const gb = Math.round((mb / 1024) * 100) / 100;
162
+ return `${gb}G`;
163
+ }
164
+ return `${mb}M`;
165
+ }
166
+
167
+ function parseMemoryMb(value: string): number {
168
+ const m = /^(\d+(?:\.\d+)?)(G|M)$/i.exec(value);
169
+ if (!m) return MIN_MEMORY_MB;
170
+ const n = Number(m[1]);
171
+ return m[2]!.toUpperCase() === "G" ? n * 1024 : n;
172
+ }
173
+
174
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
175
+ return value !== null && typeof value === "object" && !Array.isArray(value);
176
+ }
@@ -63,11 +63,18 @@ describe("loadExistingStackControls", () => {
63
63
  expect(controls?.OKE_AI_MODEL).toBe("gemma4:e4b-q4_K_M");
64
64
  });
65
65
 
66
+ test("seeds commented OKE_AI_MODEL from .env.local when .env.docker omits it", async () => {
67
+ const dir = await mkdtemp(join(tmpdir(), "oke-stack-ai-"));
68
+ await writeFile(join(dir, ".env.local"), "# OKE_AI_MODEL=gemma4:e4b-q4_K_M\n", "utf8");
69
+ const controls = await loadExistingStackControls(dir);
70
+ expect(controls?.OKE_AI_MODEL).toBe("gemma4:e4b-q4_K_M");
71
+ });
72
+
66
73
  test("prefers active OKE_AI_MODEL in docker/.env.docker over .env.local", async () => {
67
74
  const dir = await mkdtemp(join(tmpdir(), "oke-stack-ai-"));
68
75
  await mkdir(join(dir, "docker"), { recursive: true });
69
76
  await writeFile(join(dir, "docker", ".env.docker"), "OKE_AI_MODEL=smollm2\n", "utf8");
70
- await writeFile(join(dir, ".env.local"), "OKE_AI_MODEL=gemma4:e4b-q4_K_M\n", "utf8");
77
+ await writeFile(join(dir, ".env.local"), "# OKE_AI_MODEL=gemma4:e4b-q4_K_M\n", "utf8");
71
78
  const controls = await loadExistingStackControls(dir);
72
79
  expect(controls?.OKE_AI_MODEL).toBe("smollm2");
73
80
  });
@@ -132,7 +139,7 @@ describe("deriveInfrastructure instanceId", () => {
132
139
  },
133
140
  },
134
141
  });
135
- const base = result.files.find((f) => f.path === "compose.yml")!.content;
142
+ const base = result.files.find((f) => f.path === "docker-compose.yml")!.content;
136
143
  expect(base).toContain("oke-dev-abcdef");
137
144
  expect(result.stackEnv.DATABASE_URL).toMatch(/:15\d{3}\//);
138
145
  expect(result.stackEnv.REDIS_URL).toMatch(/:16\d{3}/);
@@ -163,8 +170,8 @@ describe("deriveInfrastructure instanceId", () => {
163
170
  },
164
171
  },
165
172
  });
166
- const mailYml = result.files.find((f) => f.path === "compose.channel.email.yml")!.content;
167
- const filesYml = result.files.find((f) => f.path === "compose.store.files.yml")!.content;
173
+ const mailYml = result.files.find((f) => f.path === "docker-compose.yml")!.content;
174
+ const filesYml = mailYml;
168
175
  expect(mailYml).toContain(`${20_000 + n}:1025`);
169
176
  expect(mailYml).toContain(`${21_000 + n}:8025`);
170
177
  expect(mailYml).not.toContain("8025:8025");
@@ -131,9 +131,6 @@ export const STACK_CONTROL_KEYS = [
131
131
  "OKE_AI_CTX_SIZE",
132
132
  "OKE_PROXY_HOST",
133
133
  "OKE_PROXY_ACME_EMAIL",
134
- // OpenBao app token — minted by bootstrap, must survive `writeDerivedFiles`.
135
- "OKE_VAULT_TOKEN",
136
- "OKE_VAULT_MOUNT",
137
134
  ] as const;
138
135
 
139
136
  /**
@@ -176,9 +173,6 @@ export async function loadExistingStackCredentials(
176
173
  /**
177
174
  * Load optional controls from `docker/.env.docker` when present.
178
175
  *
179
- * Falls back to `.oke/openbao/app.token` for `OKE_VAULT_TOKEN` when the env
180
- * file was regenerated without the bootstrap overlay (per-project token).
181
- *
182
176
  * @param cwd - Project root
183
177
  */
184
178
  export async function loadExistingStackControls(
@@ -192,31 +186,17 @@ export async function loadExistingStackControls(
192
186
  controls = parseStackControls(await file.text());
193
187
  break;
194
188
  }
195
- // create-oke / `oke ai setup` write the chosen model into `.env.local`.
196
- // Seed it into stack controls on first docker boot so `LLAMA_ARG_DOCKER_REPO`
197
- // is not stuck on the recipe default (`smollm2`).
189
+ // create-oke / `oke ai setup` write the chosen model into `.env.local` as a
190
+ // commented hint (active would shadow `docker/.env.docker`). Seed active or
191
+ // commented `OKE_AI_MODEL` on first docker boot so `LLAMA_ARG_DOCKER_REPO`
192
+ // is not stuck on the recipe default (`granite3.3:2b`).
198
193
  if (!controls.OKE_AI_MODEL) {
199
194
  const localEnv = Bun.file(resolve(cwd, ".env.local"));
200
195
  if (await localEnv.exists()) {
201
- const model = parseDotenv(await localEnv.text())
202
- .get("OKE_AI_MODEL")
203
- ?.trim();
196
+ const model = readEnvAssignment(await localEnv.text(), "OKE_AI_MODEL");
204
197
  if (model) controls = { ...controls, OKE_AI_MODEL: model };
205
198
  }
206
199
  }
207
- if (!controls.OKE_VAULT_TOKEN) {
208
- const tokenFile = Bun.file(resolve(cwd, ".oke", "openbao", "app.token"));
209
- if (await tokenFile.exists()) {
210
- const token = (await tokenFile.text()).trim();
211
- if (token) {
212
- controls = {
213
- ...controls,
214
- OKE_VAULT_TOKEN: token,
215
- OKE_VAULT_MOUNT: controls.OKE_VAULT_MOUNT ?? "secret",
216
- };
217
- }
218
- }
219
- }
220
200
  return Object.keys(controls).length > 0 ? controls : undefined;
221
201
  }
222
202
 
@@ -228,14 +208,44 @@ function parseDotenv(text: string): Map<string, string> {
228
208
  const eq = trimmed.indexOf("=");
229
209
  if (eq <= 0) continue;
230
210
  const key = trimmed.slice(0, eq);
231
- let value = trimmed.slice(eq + 1);
232
- if (
233
- (value.startsWith('"') && value.endsWith('"')) ||
234
- (value.startsWith("'") && value.endsWith("'"))
235
- ) {
236
- value = value.slice(1, -1);
237
- }
211
+ const value = unquoteEnvValue(trimmed.slice(eq + 1));
238
212
  map.set(key, value);
239
213
  }
240
214
  return map;
241
215
  }
216
+
217
+ /**
218
+ * Read `KEY=value` from dotenv text — active line wins; otherwise the last
219
+ * `# KEY=value` hint (create-oke / `oke ai setup` leave AI stack keys commented).
220
+ *
221
+ * @param text - Dotenv contents
222
+ * @param key - Variable name
223
+ */
224
+ function readEnvAssignment(text: string, key: string): string | undefined {
225
+ const active = parseDotenv(text).get(key)?.trim();
226
+ if (active) return active;
227
+ let hinted: string | undefined;
228
+ const prefix = `# ${key}=`;
229
+ const altPrefix = `#${key}=`;
230
+ for (const line of text.split("\n")) {
231
+ const trimmed = line.trim();
232
+ if (trimmed.startsWith(prefix)) {
233
+ hinted = unquoteEnvValue(trimmed.slice(prefix.length));
234
+ } else if (trimmed.startsWith(altPrefix)) {
235
+ hinted = unquoteEnvValue(trimmed.slice(altPrefix.length));
236
+ }
237
+ }
238
+ const value = hinted?.trim();
239
+ return value || undefined;
240
+ }
241
+
242
+ function unquoteEnvValue(raw: string): string {
243
+ let value = raw;
244
+ if (
245
+ (value.startsWith('"') && value.endsWith('"')) ||
246
+ (value.startsWith("'") && value.endsWith("'"))
247
+ ) {
248
+ value = value.slice(1, -1);
249
+ }
250
+ return value;
251
+ }
@@ -55,8 +55,8 @@ describe("oke dev --docker postgres integration", () => {
55
55
  writeStackEnv: true,
56
56
  });
57
57
 
58
- // Infra-only: network + role compose (no app build).
59
- const composeFiles = ["compose.yml", "compose.store.sql.yml"];
58
+ // Infra-only: single docker-compose.yml (no app build).
59
+ const composeFiles = ["docker-compose.yml"];
60
60
  const up = Bun.spawn(
61
61
  [
62
62
  "docker",
@@ -122,24 +122,13 @@ describe("oke dev --docker postgres integration", () => {
122
122
  }
123
123
 
124
124
  // Prove credentials live in docker/.env.docker, not YAML.
125
- const yml = await Bun.file(join(dockerDir, "compose.store.sql.yml")).text();
125
+ const yml = await Bun.file(join(dockerDir, "docker-compose.yml")).text();
126
126
  expect(yml).not.toContain("stack-integration-pass");
127
127
  expect(formatStackEnv(derived.stackEnv)).toContain("stack-integration-pass");
128
128
  expect(await Bun.file(join(dockerDir, ".env.docker")).exists()).toBe(true);
129
129
  } finally {
130
130
  await Bun.spawn(
131
- [
132
- "docker",
133
- "compose",
134
- "-p",
135
- project,
136
- "-f",
137
- "compose.yml",
138
- "-f",
139
- "compose.store.sql.yml",
140
- "down",
141
- "-v",
142
- ],
131
+ ["docker", "compose", "-p", project, "-f", "docker-compose.yml", "down", "-v"],
143
132
  { cwd: dockerDir, stdout: "pipe", stderr: "pipe" },
144
133
  ).exited.catch(() => {});
145
134
  await rm(dir, { recursive: true, force: true }).catch(() => {});
@@ -132,6 +132,15 @@ export interface ImageRecipe {
132
132
  url(spec: ServiceSpec, endpoint: ServiceEndpoint): string;
133
133
  }
134
134
 
135
+ /**
136
+ * How derived compose artefacts are laid out under {@link DeriveOptions.composeDir}.
137
+ *
138
+ * - `single` (default) — one production-grade `docker-compose.yml`
139
+ * - `split` — `compose.yml` + per-role `compose.<role>.yml` (+ `compose.prod.yml` when prod)
140
+ * - `stack` — one Swarm-oriented `docker-stack.yml`
141
+ */
142
+ export type ComposeLayout = "single" | "split" | "stack";
143
+
135
144
  /** Options for compose / Dockerfile derivation. */
136
145
  export interface DeriveOptions {
137
146
  /** Role → image pin (from `oke.config.ts` / Manifest). */
@@ -140,10 +149,30 @@ export interface DeriveOptions {
140
149
  readonly app?: string;
141
150
  /** App listen port (default 6530). */
142
151
  readonly appPort?: number;
143
- /** When true, emit prod overlays (limits, secret refs, deploy.replicas). */
152
+ /**
153
+ * When true, fold production readiness (`/_/ready` HEALTHCHECK, deploy,
154
+ * restart policy, secret refs) and apportion {@link serverCpus} /
155
+ * {@link serverMemoryGb} across services. `oke docker` defaults true;
156
+ * `oke dev --docker` keeps false.
157
+ */
144
158
  readonly prod?: boolean;
145
159
  /**
146
- * Include the `app` service in `compose.yml` (default true).
160
+ * Compose file layout (default `single` `docker-compose.yml`).
161
+ * Pass `split` for layered `compose.<role>.yml`, or `stack` for `docker-stack.yml`.
162
+ */
163
+ readonly layout?: ComposeLayout;
164
+ /**
165
+ * Host CPU count used to size `deploy.resources` when {@link prod} is true
166
+ * (default 4).
167
+ */
168
+ readonly serverCpus?: number;
169
+ /**
170
+ * Host RAM in GiB used to size `deploy.resources` when {@link prod} is true
171
+ * (default 8).
172
+ */
173
+ readonly serverMemoryGb?: number;
174
+ /**
175
+ * Include the `app` service in the compose document (default true).
147
176
  * `oke dev --docker` sets false — host Bun runs the app; Docker is infra only.
148
177
  */
149
178
  readonly includeApp?: boolean;
@@ -193,6 +222,9 @@ export interface DeriveResult {
193
222
  * never embedded in YAML.
194
223
  */
195
224
  readonly stackEnv: Readonly<Record<string, string>>;
196
- /** Compose `-f` merge order (four override layers). */
225
+ /**
226
+ * Compose `-f` merge order (includes a user-owned override that oke never
227
+ * writes — callers should skip it when the file is absent).
228
+ */
197
229
  readonly composeFiles: readonly string[];
198
230
  }
@@ -147,12 +147,12 @@ describe("ollama live — real completion", () => {
147
147
 
148
148
  const runtime = bindAi(
149
149
  {
150
- config: { drivers: { ai: { local: "ollama", test: "ollama" } } },
150
+ config: { drivers: { ai: { test: "ollama", dev: "ollama" } } },
151
151
  ai: { models: [smart], prompts: [ping] },
152
152
  },
153
153
  undefined,
154
154
  () => Date.now(),
155
- "local",
155
+ "test",
156
156
  false,
157
157
  );
158
158
  expect(runtime.prompts.has("ollama-ping")).toBe(true);