okengine 0.10.2 → 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 (343) hide show
  1. package/package.json +76 -10
  2. package/site/content/docs/ai/mcp.mdx +0 -1
  3. package/site/content/docs/deployment/docker-swarm.mdx +48 -111
  4. package/site/content/docs/deployment/docker.mdx +43 -36
  5. package/site/content/docs/deployment/index.mdx +9 -9
  6. package/site/content/docs/deployment/kubernetes.mdx +4 -4
  7. package/site/content/docs/elements/ai.mdx +7 -13
  8. package/site/content/docs/elements/channel.mdx +9 -13
  9. package/site/content/docs/elements/clock.mdx +8 -14
  10. package/site/content/docs/elements/flow.mdx +46 -54
  11. package/site/content/docs/elements/gate.mdx +6 -11
  12. package/site/content/docs/elements/signal.mdx +2 -8
  13. package/site/content/docs/elements/store.mdx +79 -93
  14. package/site/content/docs/elements/vault.mdx +89 -76
  15. package/site/content/docs/get-started/basic-usage.mdx +8 -14
  16. package/site/content/docs/get-started/installation.mdx +36 -36
  17. package/site/content/docs/get-started/introduction.mdx +11 -0
  18. package/site/content/docs/get-started/why.mdx +3 -4
  19. package/site/content/docs/index.mdx +0 -1
  20. package/site/content/docs/meta.json +0 -1
  21. package/site/content/docs/plugins/anonymous.mdx +1 -1
  22. package/site/content/docs/plugins/magic-link.mdx +11 -11
  23. package/site/content/docs/plugins/otp.mdx +2 -2
  24. package/site/content/docs/plugins/passkey.mdx +1 -1
  25. package/site/content/docs/plugins/two-factor.mdx +1 -1
  26. package/site/content/docs/plugins/username.mdx +1 -1
  27. package/site/content/docs/recipes/caddy.mdx +1 -1
  28. package/site/content/docs/recipes/cockroachdb.mdx +2 -2
  29. package/site/content/docs/recipes/dragonfly.mdx +1 -1
  30. package/site/content/docs/recipes/index.mdx +1 -2
  31. package/site/content/docs/recipes/llama-cpp.mdx +21 -23
  32. package/site/content/docs/recipes/mailpit.mdx +11 -11
  33. package/site/content/docs/recipes/meilisearch.mdx +10 -10
  34. package/site/content/docs/recipes/meta.json +0 -1
  35. package/site/content/docs/recipes/ollama.mdx +1 -1
  36. package/site/content/docs/recipes/pgdog.mdx +12 -9
  37. package/site/content/docs/recipes/postgres.mdx +3 -3
  38. package/site/content/docs/recipes/redis.mdx +2 -2
  39. package/site/content/docs/recipes/rustfs.mdx +10 -7
  40. package/site/content/docs/recipes/sglang.mdx +1 -1
  41. package/site/content/docs/recipes/supabase-docker.mdx +1 -1
  42. package/site/content/docs/recipes/timescale.mdx +2 -2
  43. package/site/content/docs/recipes/valkey.mdx +1 -1
  44. package/site/content/docs/recipes/vllm.mdx +1 -1
  45. package/site/content/docs/recipes/yugabytedb.mdx +2 -2
  46. package/site/content/docs/reference/cli.md +61 -38
  47. package/site/content/docs/reference/configuration.mdx +145 -70
  48. package/site/content/docs/reference/environment-variables.mdx +36 -28
  49. package/site/content/docs/reference/errors.mdx +28 -29
  50. package/site/content/docs/reference/fx.mdx +14 -7
  51. package/site/content/docs/reference/i18n.mdx +17 -6
  52. package/site/content/docs/reference/index.mdx +5 -0
  53. package/site/content/docs/reference/meta.json +1 -0
  54. package/site/content/docs/reference/migrating-environments.mdx +158 -0
  55. package/site/content/docs/reference/plugins.mdx +2 -2
  56. package/src/auth/gate-auth.test.ts +12 -10
  57. package/src/cli/ai-setup/ai-setup.test.ts +30 -16
  58. package/src/cli/ai-setup/apply.ts +111 -15
  59. package/src/cli/ai-setup/catalog.ts +3 -3
  60. package/src/cli/ai-setup/index.ts +1 -1
  61. package/src/cli/ai-setup/prompts.ts +1 -1
  62. package/src/cli/ai-setup/recommend.test.ts +1 -1
  63. package/src/cli/ask-seed.test.ts +3 -3
  64. package/src/cli/build.ts +8 -9
  65. package/src/cli/db-auto-push.test.ts +6 -3
  66. package/src/cli/db-auto-push.ts +7 -5
  67. package/src/cli/db-seed.ts +7 -8
  68. package/src/cli/db.test.ts +70 -71
  69. package/src/cli/db.ts +86 -12
  70. package/src/cli/dev-controller.test.ts +35 -0
  71. package/src/cli/dev-controller.ts +298 -0
  72. package/src/cli/dev-controls.ts +11 -65
  73. package/src/cli/dev-db-push.test.ts +47 -15
  74. package/src/cli/dev-schema-sync.test.ts +11 -11
  75. package/src/cli/dev-schema-sync.ts +10 -10
  76. package/src/cli/dev-session-lock.test.ts +106 -0
  77. package/src/cli/dev-session-lock.ts +209 -0
  78. package/src/cli/dev.test.ts +246 -114
  79. package/src/cli/dev.ts +153 -205
  80. package/src/cli/doc-staleness.test.ts +3 -6
  81. package/src/cli/docker-clean.test.ts +33 -1
  82. package/src/cli/docker-clean.ts +28 -33
  83. package/src/cli/docker-cli.test.ts +12 -11
  84. package/src/cli/docker.ts +95 -20
  85. package/src/cli/doctor.test.ts +40 -0
  86. package/src/cli/doctor.ts +142 -9
  87. package/src/cli/drizzle-env.test.ts +11 -13
  88. package/src/cli/drizzle-env.ts +18 -11
  89. package/src/cli/ensure-drizzle-config.ts +11 -14
  90. package/src/cli/hero-meta.test.ts +15 -15
  91. package/src/cli/hero-meta.ts +10 -9
  92. package/src/cli/index.ts +36 -6
  93. package/src/cli/load-config.images.test.ts +44 -10
  94. package/src/cli/load-config.source.test.ts +50 -0
  95. package/src/cli/load-config.ts +35 -29
  96. package/src/cli/meilisearch-local.ts +1 -2
  97. package/src/cli/mode.ts +7 -58
  98. package/src/cli/registry.ts +175 -19
  99. package/src/cli/resolve-dev-sql-env.test.ts +8 -16
  100. package/src/cli/resolve-dev-sql-env.ts +13 -15
  101. package/src/cli/safe-defaults.test.ts +29 -11
  102. package/src/cli/schema.ts +65 -16
  103. package/src/cli/test.ts +58 -0
  104. package/src/cli/tui/App.tsx +252 -0
  105. package/src/cli/tui/DevLive.tsx +116 -0
  106. package/src/cli/tui/components/ConfirmDialog.tsx +48 -0
  107. package/src/cli/tui/components/HeaderBar.tsx +67 -0
  108. package/src/cli/tui/components/LogStream.tsx +47 -0
  109. package/src/cli/tui/components/PanelCard.tsx +39 -0
  110. package/src/cli/tui/components/SelectRow.tsx +37 -0
  111. package/src/cli/tui/components/SlashPalette.tsx +172 -0
  112. package/src/cli/tui/components/Spinner.tsx +32 -0
  113. package/src/cli/tui/components/StatusBar.tsx +51 -0
  114. package/src/cli/tui/components/StatusDot.tsx +37 -0
  115. package/src/cli/tui/components/TabBar.tsx +49 -0
  116. package/src/cli/tui/components/Wordmark.tsx +43 -0
  117. package/src/cli/tui/format.ts +38 -0
  118. package/src/cli/tui/keys.test.ts +18 -0
  119. package/src/cli/tui/keys.ts +26 -0
  120. package/src/cli/tui/launch.test.ts +18 -0
  121. package/src/cli/tui/launch.ts +57 -0
  122. package/src/cli/tui/panels/Dashboard.tsx +201 -0
  123. package/src/cli/tui/panels/Database.tsx +198 -0
  124. package/src/cli/tui/panels/Dev.tsx +257 -0
  125. package/src/cli/tui/panels/Docker.tsx +357 -0
  126. package/src/cli/tui/panels/Navigator.test.ts +22 -0
  127. package/src/cli/tui/panels/Navigator.tsx +213 -0
  128. package/src/cli/tui/slash-catalog.test.ts +31 -0
  129. package/src/cli/tui/slash-catalog.ts +184 -0
  130. package/src/cli/tui/slash-run.ts +54 -0
  131. package/src/cli/tui/state/ports.ts +95 -0
  132. package/src/cli/tui/theme.ts +24 -0
  133. package/src/cli/vault-cmd.test.ts +402 -0
  134. package/src/cli/vault-cmd.ts +727 -69
  135. package/src/cli/vault-secure-input.test.ts +56 -0
  136. package/src/cli/vault-secure-input.ts +124 -0
  137. package/src/compiler/effects-infer.ts +7 -0
  138. package/src/compiler/extract.ts +12 -0
  139. package/src/compiler/fixtures/skyport/oke.config.ts +7 -5
  140. package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -1
  141. package/src/compiler/fixtures/skyport.expected.json +1 -1
  142. package/src/compiler/generate-adopt.ts +27 -1
  143. package/src/compiler-entry.ts +7 -0
  144. package/src/config/define-config.test.ts +111 -47
  145. package/src/config/driver-defaults.test.ts +115 -0
  146. package/src/config/driver-defaults.ts +156 -0
  147. package/src/config/index.ts +246 -131
  148. package/src/config/resolve-driver.test.ts +14 -19
  149. package/src/console/server/app.ts +4 -2
  150. package/src/console/server/console.test.ts +58 -1
  151. package/src/console/server/flows.ts +22 -2
  152. package/src/console/server/index.ts +3 -0
  153. package/src/console/server/operator-db.test.ts +55 -13
  154. package/src/console/server/operator-db.ts +305 -263
  155. package/src/console/server/serve.ts +16 -7
  156. package/src/console/server/state.ts +31 -9
  157. package/src/console/server/vault.test.ts +120 -5
  158. package/src/console/server/vault.ts +208 -12
  159. package/src/console/ui/dist/assets/{index-D-9KFce5.js → index--Y0Uaz-m.js} +2 -2
  160. package/src/console/ui/dist/assets/panel-vault-BkZdJPX8.js +1 -0
  161. package/src/console/ui/dist/assets/style-C3L349iZ.css +3 -0
  162. package/src/console/ui/dist/index.html +2 -2
  163. package/src/console/ui/shell/client.ts +16 -0
  164. package/src/console/ui/shell/panels/vault/VaultPanel.tsx +54 -1
  165. package/src/console/ui/vault/VaultA11yView.tsx +33 -1
  166. package/src/console/ui/vault/backend.test.ts +75 -0
  167. package/src/console/ui/vault/backend.ts +117 -0
  168. package/src/console/ui/vault/fixture.ts +21 -2
  169. package/src/console/ui/vault/index.ts +11 -1
  170. package/src/console/ui/vault/types.ts +26 -0
  171. package/src/docker/compose.ts +164 -61
  172. package/src/docker/derive.ts +106 -12
  173. package/src/docker/docker.test.ts +156 -110
  174. package/src/docker/helpers.ts +98 -0
  175. package/src/docker/images-config.test.ts +163 -0
  176. package/src/docker/index.ts +30 -5
  177. package/src/docker/recipes/index.ts +0 -3
  178. package/src/docker/recipes/llama-cpp.ts +4 -4
  179. package/src/docker/recipes/pgdog.ts +9 -2
  180. package/src/docker/resources.test.ts +71 -0
  181. package/src/docker/resources.ts +176 -0
  182. package/src/docker/stack-id.test.ts +11 -4
  183. package/src/docker/stack-id.ts +42 -32
  184. package/src/docker/stack.integration.test.ts +4 -15
  185. package/src/docker/types.ts +35 -3
  186. package/src/drivers/ai-ollama.integration.test.ts +2 -2
  187. package/src/drivers/conformance.test.ts +11 -12
  188. package/src/drivers/drizzle-dialect.test.ts +0 -4
  189. package/src/drivers/drizzle-dialect.ts +7 -13
  190. package/src/drivers/index.ts +24 -7
  191. package/src/drivers/journal-postgres.test.ts +3 -1
  192. package/src/drivers/journal-postgres.ts +4 -2
  193. package/src/drivers/meilisearch.ts +1 -1
  194. package/src/drivers/pglite.ts +28 -5
  195. package/src/drivers/types.ts +9 -11
  196. package/src/drivers/vault-aws-secrets-manager.test.ts +159 -0
  197. package/src/drivers/vault-aws-secrets-manager.ts +333 -0
  198. package/src/drivers/vault-builtin.test.ts +110 -0
  199. package/src/drivers/vault-builtin.ts +184 -0
  200. package/src/drivers/vault-driver-removal.test.ts +13 -0
  201. package/src/drivers/vault-managed.test.ts +95 -0
  202. package/src/drivers/vault-managed.ts +101 -25
  203. package/src/drivers/vault-types.ts +25 -5
  204. package/src/elements/ai/runtime.ts +1 -1
  205. package/src/elements/clock/chaos.test.ts +4 -1
  206. package/src/elements/clock/durable.ts +57 -4
  207. package/src/elements/clock.test.ts +13 -14
  208. package/src/elements/gate/constants.ts +18 -0
  209. package/src/elements/gate.test.ts +1 -1
  210. package/src/elements/gate.ts +2 -2
  211. package/src/elements/store/domain-ddl.test.ts +5 -12
  212. package/src/elements/store/emit-drizzle.ts +44 -19
  213. package/src/elements/store/index-boot.test.ts +15 -116
  214. package/src/elements/store/load-plugin-tables.ts +1 -1
  215. package/src/elements/store/resource-list-docs.fixture.ts +12 -6
  216. package/src/elements/store/resource-list-docs.test.ts +64 -44
  217. package/src/elements/store/resource.test.ts +17 -10
  218. package/src/elements/store/runtime.ts +9 -12
  219. package/src/elements/store/schema-decl.test.ts +16 -32
  220. package/src/elements/store/seed.test.ts +1 -2
  221. package/src/elements/store/seed.ts +7 -8
  222. package/src/elements/store/sql-condition.test.ts +2 -2
  223. package/src/elements/store/sql-session.test.ts +8 -10
  224. package/src/elements/store/sql-session.ts +10 -2
  225. package/src/elements/store.test.ts +13 -13
  226. package/src/elements/vault/audit.ts +233 -0
  227. package/src/elements/vault/boot-chain.test.ts +78 -0
  228. package/src/elements/vault/boot-chain.ts +43 -39
  229. package/src/elements/vault/builtin-adapter.test.ts +479 -0
  230. package/src/elements/vault/builtin-adapter.ts +1184 -0
  231. package/src/elements/vault/crypto.test.ts +237 -0
  232. package/src/elements/vault/crypto.ts +431 -0
  233. package/src/elements/vault/declare.ts +164 -3
  234. package/src/elements/vault/errors.ts +56 -0
  235. package/src/elements/vault/kms-unseal.test.ts +76 -0
  236. package/src/elements/vault/kms-unseal.ts +181 -0
  237. package/src/elements/vault/path.ts +90 -0
  238. package/src/elements/vault/resilience.test.ts +102 -0
  239. package/src/elements/vault/resilience.ts +107 -0
  240. package/src/elements/vault/runtime.ts +36 -4
  241. package/src/elements/vault/security-checklist.test.ts +130 -0
  242. package/src/elements/vault/storage.ts +448 -0
  243. package/src/elements/vault/types.ts +288 -0
  244. package/src/elements/vault/unseal.ts +150 -0
  245. package/src/elements/vault.test.ts +72 -4
  246. package/src/elements/vault.ts +91 -2
  247. package/src/full.ts +248 -0
  248. package/src/http.ts +41 -0
  249. package/src/i18n/interpolate-simple.ts +37 -0
  250. package/src/i18n/messages.ts +51 -4
  251. package/src/i18n-entry.ts +7 -0
  252. package/src/index.ts +35 -145
  253. package/src/journal-entry.ts +20 -0
  254. package/src/kernel/adopt-barrel-fresh.test.ts +25 -10
  255. package/src/kernel/app.ts +40 -42
  256. package/src/kernel/auto-registry.test.ts +2 -2
  257. package/src/kernel/boot-bind/ai.test.ts +5 -5
  258. package/src/kernel/boot-bind/ai.ts +2 -1
  259. package/src/kernel/boot-bind/channel.test.ts +6 -6
  260. package/src/kernel/boot-bind/channel.ts +5 -4
  261. package/src/kernel/boot-bind/clock.ts +5 -5
  262. package/src/kernel/boot-bind/gate.ts +6 -7
  263. package/src/kernel/boot-bind/honor-config.test.ts +42 -42
  264. package/src/kernel/boot-bind/journal.ts +5 -5
  265. package/src/kernel/boot-bind/signal.ts +3 -1
  266. package/src/kernel/boot-bind/store.test.ts +55 -37
  267. package/src/kernel/boot-bind/store.ts +24 -34
  268. package/src/kernel/boot-bind/vault.ts +19 -4
  269. package/src/kernel/boot.test.ts +12 -4
  270. package/src/kernel/boot.ts +31 -19
  271. package/src/kernel/budget-entry.ts +1 -1
  272. package/src/kernel/compensate.test.ts +288 -1
  273. package/src/kernel/compensate.ts +126 -0
  274. package/src/kernel/concurrency.test.ts +10 -9
  275. package/src/kernel/effects-stamping.test.ts +27 -2
  276. package/src/kernel/element-registries.ts +5 -3
  277. package/src/kernel/flow.ts +4 -2
  278. package/src/kernel/fx.test.ts +119 -3
  279. package/src/kernel/fx.ts +215 -40
  280. package/src/kernel/index.ts +14 -0
  281. package/src/kernel/journal.ts +130 -7
  282. package/src/kernel/pipeline.test.ts +20 -6
  283. package/src/kernel/redacted.test.ts +35 -0
  284. package/src/kernel/redacted.ts +24 -2
  285. package/src/kernel/router/create-default.ts +16 -0
  286. package/src/kernel/router/create-edge.ts +18 -0
  287. package/src/kernel/router/create.ts +23 -0
  288. package/src/kernel/router/index.ts +23 -0
  289. package/src/kernel/router/linear.ts +88 -0
  290. package/src/kernel/router/regexp.ts +188 -0
  291. package/src/kernel/router/smart.ts +120 -0
  292. package/src/kernel/router/trie.ts +108 -0
  293. package/src/kernel/router/types.ts +56 -0
  294. package/src/kernel/router.ts +18 -532
  295. package/src/kernel-entry.ts +43 -0
  296. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  297. package/src/manifest/fixtures/skyport.manifest.json +1 -1
  298. package/src/plugins/config-source.test.ts +7 -7
  299. package/src/release/build-lib.ts +58 -0
  300. package/src/release/exports.test.ts +4 -2
  301. package/src/release/exports.ts +30 -4
  302. package/src/release/measure.ts +101 -22
  303. package/src/runs/duckdb.ts +7 -2
  304. package/src/shared/optional-peer.ts +41 -0
  305. package/src/term.test.ts +3 -44
  306. package/src/test/create-test-app.ts +12 -0
  307. package/src/test/provisions.integration.test.ts +1 -1
  308. package/src/test/reset-element-registries.ts +2 -0
  309. package/src/upgrade/codemods.test.ts +7 -20
  310. package/src/upgrade/codemods.ts +93 -60
  311. package/site/content/docs/console/access.mdx +0 -29
  312. package/site/content/docs/console/ai.mdx +0 -35
  313. package/site/content/docs/console/architecture.mdx +0 -35
  314. package/site/content/docs/console/channels.mdx +0 -37
  315. package/site/content/docs/console/clock.mdx +0 -51
  316. package/site/content/docs/console/flows.mdx +0 -31
  317. package/site/content/docs/console/gates.mdx +0 -73
  318. package/site/content/docs/console/index.mdx +0 -54
  319. package/site/content/docs/console/manifest-diff.mdx +0 -34
  320. package/site/content/docs/console/meta.json +0 -24
  321. package/site/content/docs/console/overview.mdx +0 -40
  322. package/site/content/docs/console/plugins.mdx +0 -41
  323. package/site/content/docs/console/privacy.mdx +0 -32
  324. package/site/content/docs/console/runs.mdx +0 -50
  325. package/site/content/docs/console/signals.mdx +0 -40
  326. package/site/content/docs/console/store.mdx +0 -32
  327. package/site/content/docs/console/tenancy.mdx +0 -32
  328. package/site/content/docs/console/traces.mdx +0 -38
  329. package/site/content/docs/console/vault.mdx +0 -41
  330. package/site/content/docs/recipes/openbao.mdx +0 -141
  331. package/src/cli/ask-dev-mode.ts +0 -40
  332. package/src/cli/dev-mode.test.ts +0 -45
  333. package/src/cli/dev-mode.ts +0 -78
  334. package/src/cli/openbao-bootstrap.test.ts +0 -150
  335. package/src/cli/openbao-bootstrap.ts +0 -321
  336. package/src/cli/openbao-restart.integration.test.ts +0 -145
  337. package/src/console/ui/dist/assets/panel-vault-Za1GyJfM.js +0 -1
  338. package/src/console/ui/dist/assets/style-C8MxEWPd.css +0 -3
  339. package/src/docker/recipes/openbao.ts +0 -47
  340. package/src/drivers/libsql.ts +0 -179
  341. package/src/drivers/sqlite.ts +0 -44
  342. package/src/drivers/vault-openbao.test.ts +0 -97
  343. package/src/drivers/vault-openbao.ts +0 -143
@@ -0,0 +1,163 @@
1
+ /**
2
+ * `ImagesConfig` (config surface, nested) → internal flat role map.
3
+ *
4
+ * Two guarantees:
5
+ * 1. Every built-in image recipe is reachable through the nested shape,
6
+ * correctly placed under its owning element (or flat, for roles with no
7
+ * driver counterpart) — the type-safety gap this closes.
8
+ * 2. A nested config produces byte-identical `deriveInfrastructure()` output
9
+ * (Dockerfile / compose / stack env) to the flat-shape equivalent it
10
+ * replaces — nesting is a config-surface change only, never an internal
11
+ * representation change.
12
+ */
13
+
14
+ import { describe, expect, test } from "bun:test";
15
+ import { flattenImagesConfig, type ImagesConfig } from "../config/index.ts";
16
+ import { deriveInfrastructure } from "./derive.ts";
17
+ import { builtinRecipes, recipeFor } from "./recipes/index.ts";
18
+ import type { ServiceCredentials } from "./types.ts";
19
+
20
+ describe("ImagesConfig nesting — recipe coverage", () => {
21
+ test("every built-in recipe id is reachable, correctly nested/flat by role", () => {
22
+ const cases: ReadonlyArray<{ images: ImagesConfig; role: string; image: string }> = [
23
+ // store.sql — postgres-family
24
+ {
25
+ images: { store: { sql: "postgres:18-alpine" } },
26
+ role: "store.sql",
27
+ image: "postgres:18-alpine",
28
+ },
29
+ {
30
+ images: { store: { sql: "cockroachdb/cockroach:v24.1.0" } },
31
+ role: "store.sql",
32
+ image: "cockroachdb/cockroach:v24.1.0",
33
+ },
34
+ {
35
+ images: { store: { sql: "yugabytedb/yugabyte:2024.1.0.0-b1" } },
36
+ role: "store.sql",
37
+ image: "yugabytedb/yugabyte:2024.1.0.0-b1",
38
+ },
39
+ {
40
+ images: { store: { sql: "timescale/timescaledb:2.15.0-pg16" } },
41
+ role: "store.sql",
42
+ image: "timescale/timescaledb:2.15.0-pg16",
43
+ },
44
+ {
45
+ images: { store: { sql: "supabase/postgres:15.1.0.117" } },
46
+ role: "store.sql",
47
+ image: "supabase/postgres:15.1.0.117",
48
+ },
49
+ // store.kv — redis-family
50
+ { images: { store: { kv: "redis:8-alpine" } }, role: "store.kv", image: "redis:8-alpine" },
51
+ {
52
+ images: { store: { kv: "valkey/valkey:8-alpine" } },
53
+ role: "store.kv",
54
+ image: "valkey/valkey:8-alpine",
55
+ },
56
+ {
57
+ images: { store: { kv: "docker.dragonflydb.io/dragonflydb/dragonfly:v1.24.0" } },
58
+ role: "store.kv",
59
+ image: "docker.dragonflydb.io/dragonflydb/dragonfly:v1.24.0",
60
+ },
61
+ // store.files / store.index
62
+ {
63
+ images: { store: { files: "rustfs/rustfs:1.0.0-beta.11" } },
64
+ role: "store.files",
65
+ image: "rustfs/rustfs:1.0.0-beta.11",
66
+ },
67
+ {
68
+ images: { store: { index: "getmeili/meilisearch:v1.37" } },
69
+ role: "store.index",
70
+ image: "getmeili/meilisearch:v1.37",
71
+ },
72
+ // channel.email
73
+ {
74
+ images: { channel: { email: "axllent/mailpit:v1.22.3" } },
75
+ role: "channel.email",
76
+ image: "axllent/mailpit:v1.22.3",
77
+ },
78
+ // flat roles — no driver counterpart in DriversConfig
79
+ {
80
+ images: { ai: "ghcr.io/ggml-org/llama.cpp:server-b10290" },
81
+ role: "ai",
82
+ image: "ghcr.io/ggml-org/llama.cpp:server-b10290",
83
+ },
84
+ { images: { ai: "vllm/vllm-openai:v0.26.0" }, role: "ai", image: "vllm/vllm-openai:v0.26.0" },
85
+ {
86
+ images: { ai: "lmsysorg/sglang:v0.5.16-runtime" },
87
+ role: "ai",
88
+ image: "lmsysorg/sglang:v0.5.16-runtime",
89
+ },
90
+ { images: { ai: "ollama/ollama:0.32.6" }, role: "ai", image: "ollama/ollama:0.32.6" },
91
+ {
92
+ images: { pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.51" },
93
+ role: "pgdog",
94
+ image: "ghcr.io/pgdogdev/pgdog:v0.1.51",
95
+ },
96
+ { images: { proxy: "caddy:2-alpine" }, role: "proxy", image: "caddy:2-alpine" },
97
+ { images: { proxy: "traefik:v3.1" }, role: "proxy", image: "traefik:v3.1" },
98
+ ];
99
+
100
+ const matchedIds = new Set<string>();
101
+ for (const { images, role, image } of cases) {
102
+ // The nested literal flattens to exactly the dotted/flat role key the
103
+ // rest of the pipeline (buildSpecs, credentials, env prefixes) expects.
104
+ expect(flattenImagesConfig(images)).toEqual({ [role]: image });
105
+ matchedIds.add(recipeFor(image).id);
106
+ }
107
+
108
+ expect(matchedIds).toEqual(new Set(builtinRecipes.map((r) => r.id)));
109
+ });
110
+ });
111
+
112
+ describe("ImagesConfig nesting — compose output parity", () => {
113
+ test("nested config derives byte-identical output to its flat-shape equivalent", () => {
114
+ const flatImages: Readonly<Record<string, string>> = {
115
+ "store.sql": "postgres:18-alpine",
116
+ pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.51",
117
+ "store.kv": "redis:8-alpine",
118
+ "store.files": "rustfs/rustfs:1.0.0-beta.11",
119
+ "store.index": "getmeili/meilisearch:v1.37",
120
+ "channel.email": "axllent/mailpit:v1.22.3",
121
+ };
122
+
123
+ const nestedImages: ImagesConfig = {
124
+ store: {
125
+ sql: "postgres:18-alpine",
126
+ kv: "redis:8-alpine",
127
+ files: "rustfs/rustfs:1.0.0-beta.11",
128
+ index: "getmeili/meilisearch:v1.37",
129
+ },
130
+ channel: { email: "axllent/mailpit:v1.22.3" },
131
+ pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.51",
132
+ };
133
+
134
+ // Same nested→flat set, written in a different field order — proves the
135
+ // parity holds independent of source object key order.
136
+ expect(flattenImagesConfig(nestedImages)).toEqual(flatImages);
137
+
138
+ const credentials: Readonly<Record<string, ServiceCredentials>> = {
139
+ "store.sql": { user: "oke", password: "fixed-pw-sql", database: "oke" },
140
+ "store.kv": { user: "oke", password: "fixed-pw-kv", database: "oke" },
141
+ "store.files": { user: "oke", password: "fixed-pw-files", database: "oke" },
142
+ "store.index": { user: "oke", password: "fixed-pw-index", database: "oke" },
143
+ "channel.email": { user: "oke", password: "fixed-pw-email", database: "oke" },
144
+ pgdog: { user: "oke", password: "fixed-pw-pgdog", database: "oke" },
145
+ };
146
+
147
+ const fromFlat = deriveInfrastructure({
148
+ images: flatImages,
149
+ app: "images-nesting-parity",
150
+ credentials,
151
+ });
152
+ const fromNested = deriveInfrastructure({
153
+ images: flattenImagesConfig(nestedImages),
154
+ app: "images-nesting-parity",
155
+ credentials,
156
+ });
157
+
158
+ expect(fromNested.specs).toEqual(fromFlat.specs);
159
+ expect(fromNested.files).toEqual(fromFlat.files);
160
+ expect(fromNested.stackEnv).toEqual(fromFlat.stackEnv);
161
+ expect(fromNested.composeFiles).toEqual(fromFlat.composeFiles);
162
+ });
163
+ });
@@ -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");