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
package/src/cli/dev.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `oke dev` — watch · hot reload · Console :6533 · MCP :6535 · client types on save.
3
- * Mode: `local` (in-memory) or `docker` (compose infra + host Bun).
3
+ * Always starts Docker Compose infra and runs the app on host Bun.
4
4
  */
5
5
 
6
6
  import { watch } from "node:fs";
@@ -19,7 +19,6 @@ import {
19
19
  stackAppSlug,
20
20
  stackInstanceId,
21
21
  writeDerivedFiles,
22
- formatStackEnv,
23
22
  type DeriveOptions,
24
23
  } from "../docker/index.ts";
25
24
  import type { Manifest } from "../manifest/types.ts";
@@ -45,19 +44,13 @@ import {
45
44
  } from "../term.ts";
46
45
  import { clientAdd } from "./client-add.ts";
47
46
  import { resolveDriverId } from "../config/index.ts";
48
- import { askDevMode, type AskDevModeFn } from "./ask-dev-mode.ts";
49
- import {
50
- formatDevControlsHint,
51
- startDevControls,
52
- type DevComposeControlAction,
53
- } from "./dev-controls.ts";
47
+ import type { DevComposeControlAction } from "./dev-controls.ts";
54
48
  import {
55
49
  createDebouncedRunner,
56
50
  isDomainSchemaWatchPath,
57
51
  resolveDevAutoPush,
58
52
  } from "./db-auto-push.ts";
59
53
  import { resolveDrizzleConfigPath, runPush } from "./db.ts";
60
- import { readDevMode, shouldAskDevMode, writeDevMode, type DevMode } from "./dev-mode.ts";
61
54
  import {
62
55
  buildDevHeroSnapshot,
63
56
  encodeHeroSnapshot,
@@ -68,6 +61,7 @@ import {
68
61
  import { loadManifest, loadOkeConfig, resolveImages } from "./load-config.ts";
69
62
  import { mcpContextFromConsole } from "./mcp-from-console.ts";
70
63
  import { resolveDevPorts } from "./ports.ts";
64
+ import { clearDevSessionLock, writeDevSessionLock } from "./dev-session-lock.ts";
71
65
 
72
66
  /** Max wait for the `bun --hot` app child to bind a port. */
73
67
  const APP_READY_TIMEOUT_MS = 30_000;
@@ -153,12 +147,10 @@ export interface DevOptions {
153
147
  readonly cwd?: string;
154
148
  readonly entry?: string;
155
149
  /**
156
- * Session-only mode override. `true` / role list docker; `false` local.
157
- * When unset, resolve from `.oke/mode` / TTY ask / non-TTY default `local`.
150
+ * Optional compose role filter. `true` / unsetall roles from images;
151
+ * a string list only those roles.
158
152
  */
159
153
  readonly docker?: boolean | readonly string[];
160
- /** Force local for this session only (never writes `.oke/mode`). */
161
- readonly local?: boolean;
162
154
  /**
163
155
  * Opt out of auto `oke db push` on schema change for this session.
164
156
  * Also set via `--no-db-push`.
@@ -286,14 +278,16 @@ export interface DevOptions {
286
278
  * app code while preserving the listen socket.
287
279
  */
288
280
  readonly startApp?: (entry: string, env: Record<string, string>) => Promise<DevAppHandle>;
289
- /**
290
- * Injectable mode prompt (tests). Default: {@link askDevMode}.
291
- */
292
- readonly ask?: AskDevModeFn;
293
281
  /**
294
282
  * Override stdin TTY detection (tests). Default: `process.stdin.isTTY`.
295
283
  */
296
284
  readonly stdinIsTTY?: boolean;
285
+ /**
286
+ * Probe Docker daemon availability (injectable). Default: `docker info`.
287
+ * Skipped on {@link dryRun}; when {@link composeUp} is injected and this is
288
+ * omitted, the probe is a no-op (unit tests never shell out).
289
+ */
290
+ readonly assertDocker?: () => Promise<void>;
297
291
  }
298
292
 
299
293
  /** Result of a dry-run / prepared / live-but-detached dev session. */
@@ -411,42 +405,16 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
411
405
  return { code: 1 };
412
406
  }
413
407
 
414
- const explicitLocal = options.local === true;
415
- const explicitDocker = options.docker === true || Array.isArray(options.docker);
416
- if (explicitLocal && explicitDocker) {
417
- console.error("oke dev: use either --local or --docker, not both");
418
- restoreWarn();
419
- return { code: 1 };
420
- }
421
-
422
- const savedMode = await readDevMode(cwd);
423
- const explicit = explicitLocal || explicitDocker;
424
- let mode: DevMode;
425
- if (explicitLocal) {
426
- mode = "local";
427
- } else if (explicitDocker) {
428
- mode = "docker";
429
- } else if (savedMode !== null) {
430
- mode = savedMode;
431
- } else if (
432
- shouldAskDevMode({
433
- saved: savedMode,
434
- explicit,
435
- stdinIsTTY: options.stdinIsTTY ?? process.stdin.isTTY,
436
- })
437
- ) {
438
- const ask = options.ask ?? askDevMode;
439
- const chosen = await ask();
440
- if (chosen === null) {
441
- console.error("oke dev: cancelled");
408
+ // Fail fast when the Docker daemon is down (skip on dryRun / injected compose).
409
+ if (!options.dryRun) {
410
+ const probe = options.assertDocker ?? (options.composeUp ? async () => {} : assertDockerDaemon);
411
+ try {
412
+ await probe();
413
+ } catch (err) {
414
+ console.error(err instanceof Error ? err.message : String(err));
442
415
  restoreWarn();
443
416
  return { code: 1 };
444
417
  }
445
- mode = chosen;
446
- await writeDevMode(cwd, mode);
447
- } else {
448
- // Non-TTY + unset: deterministic local — zero ask, zero docker, no save.
449
- mode = "local";
450
418
  }
451
419
 
452
420
  // Paint chrome immediately so docker/vault work is never a blank 6s wait.
@@ -459,7 +427,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
459
427
  } catch {
460
428
  // shipped binary may not sit next to package.json
461
429
  }
462
- const earlyProfile = resolveDevProfile({ docker: mode === "docker", nodeEnv: "development" });
430
+ const earlyProfile = resolveDevProfile({ docker: true, nodeEnv: "development" });
463
431
  write(
464
432
  formatDevBanner({
465
433
  profile: earlyProfile,
@@ -509,26 +477,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
509
477
  loadedConfig = null;
510
478
  }
511
479
 
512
- // Local mode + a configured `meilisearch` store.index: bring the server up
513
- // as a fourth local process (binary on PATH — a documented prerequisite,
514
- // like Docker for --docker). Docker mode instead relies on the recipe.
515
- let meili: import("./meilisearch-local.ts").LocalMeilisearchHandle | null = null;
516
- if (mode === "local") {
517
- const indexId = resolveDriverId(loadedConfig?.drivers?.store?.index, "local") ?? "memory";
518
- if (indexId === "meilisearch") {
519
- try {
520
- const { startLocalMeilisearch } = await import("./meilisearch-local.ts");
521
- meili = await startLocalMeilisearch({ cwd });
522
- write(formatStatusLine(`meilisearch local server on ${meili.url}`));
523
- } catch (err) {
524
- console.error(err instanceof Error ? err.message : String(err));
525
- restoreWarn();
526
- return { code: 1 };
527
- }
528
- }
529
- }
530
-
531
- if (mode === "docker") {
480
+ {
532
481
  let images = options.images;
533
482
  if (!images) {
534
483
  if (!loadedConfig) {
@@ -573,6 +522,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
573
522
  images,
574
523
  app: appSlug,
575
524
  prod: false,
525
+ layout: "single",
576
526
  includeApp: false,
577
527
  composeDir,
578
528
  instanceId,
@@ -580,11 +530,15 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
580
530
  ...(controls ? { controls } : {}),
581
531
  host: "127.0.0.1",
582
532
  });
583
- // Layer 4 is user-owned — include only if the file already exists.
584
- const existingOverride = await Bun.file(resolve(dockerOut, "compose.override.yml")).exists();
585
- composeFiles = derived.composeFiles.filter(
586
- (f) => f !== "compose.override.yml" || existingOverride,
587
- );
533
+ // User-owned overrides — include only if the file already exists.
534
+ composeFiles = [];
535
+ for (const f of derived.composeFiles) {
536
+ if (f === "compose.override.yml" || f === "docker-compose.override.yml") {
537
+ if (await Bun.file(resolve(dockerOut, f)).exists()) composeFiles.push(f);
538
+ continue;
539
+ }
540
+ composeFiles.push(f);
541
+ }
588
542
  stackEnv = { ...derived.stackEnv };
589
543
 
590
544
  const roleLabel = (role: string): string => {
@@ -648,7 +602,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
648
602
  if (code !== 0) {
649
603
  const detail = [stdout.trim(), stderr.trim()].filter(Boolean).join("\n");
650
604
  throw new Error(
651
- `oke dev --docker: docker compose exited ${code}` + (detail ? `\n${detail}` : ""),
605
+ `oke dev: docker compose exited ${code}` + (detail ? `\n${detail}` : ""),
652
606
  );
653
607
  }
654
608
  });
@@ -663,27 +617,6 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
663
617
  env: { ...stackEnv! },
664
618
  };
665
619
 
666
- // Real OpenBao vault: init once / unseal every start, then expose the
667
- // least-privilege app token to the app + Console (never root).
668
- const vaultSpec = derived.specs.find((s) => s.role === "vault");
669
- if (vaultSpec) {
670
- bootProgress.set("vault", formatStatusLine("vault unseal…", undefined, "pending"));
671
- const { ensureOpenBao, openbaoStackEnv } = await import("./openbao-bootstrap.ts");
672
- const vaultNames = await tryLoadVaultSecretNames(cwd);
673
- const boot = await ensureOpenBao({
674
- cwd,
675
- url: `http://127.0.0.1:${vaultSpec.hostPort}`,
676
- names: vaultNames,
677
- });
678
- Object.assign(stackEnv!, openbaoStackEnv(boot));
679
- Object.assign(dockerStarted.env, openbaoStackEnv(boot));
680
- // Rewrite .env.docker with the full stack env (including token) so a
681
- // later ensureDockerStack / writeDerivedFiles does not drop it.
682
- const envPath = resolve(dockerOut, ".env.docker");
683
- await Bun.write(envPath, formatStackEnv(stackEnv!));
684
- bootProgress.set("vault", formatStatusLine("vault ready", undefined, "ready"));
685
- }
686
-
687
620
  // Stream compose health until infra is up (AI may stay pending — model load).
688
621
  const aiServiceName = derived.specs.find((s) => s.role === "ai")?.serviceName;
689
622
  const labelForService = (serviceName: string): string => {
@@ -747,7 +680,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
747
680
  const model =
748
681
  stackEnv!.OKE_AI_MODEL?.trim() ||
749
682
  process.env.OKE_AI_MODEL?.trim() ||
750
- (recipeId === "ollama" ? "qwen3.5:9b" : "smollm2");
683
+ (recipeId === "ollama" ? "qwen3.5:9b" : "granite3.3:2b");
751
684
  const url = `http://127.0.0.1:${aiSpec.hostPort}`;
752
685
  const paintAi = (status: DevStatus, detail?: string) => {
753
686
  heroAiStatus = status;
@@ -807,7 +740,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
807
740
  } catch (err) {
808
741
  const msg = err instanceof Error ? err.message : String(err);
809
742
  console.error(msg);
810
- console.error("oke dev: docker mode failed — fix compose, or run `oke mode local`");
743
+ console.error("oke dev: Docker Compose failed — fix compose and retry");
811
744
  restoreWarn();
812
745
  return { code: 1 };
813
746
  }
@@ -833,9 +766,9 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
833
766
  }
834
767
  const heroSnapshot = buildDevHeroSnapshot({
835
768
  config: loadedConfig,
836
- docker: mode === "docker",
837
- sqlDriver: mode === "docker" ? stackSqlDriver : undefined,
838
- kvDriver: mode === "docker" ? stackKvDriver : undefined,
769
+ docker: true,
770
+ sqlDriver: stackSqlDriver,
771
+ kvDriver: stackKvDriver,
839
772
  aiModel: heroAiModel,
840
773
  aiStatus: heroAiStatus,
841
774
  version: okeVersion,
@@ -950,7 +883,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
950
883
  bootProgress.clear();
951
884
  const bootBoard = createAnchoredBoard(write);
952
885
  bootBoard.paint(paintBootBoard(heroAiStatus));
953
- // Track everything printed below so can rewrite in place for the session.
886
+ // Track lines printed below the board so live repaints stay anchored.
954
887
  write = bootBoard.wrapWrite(write);
955
888
 
956
889
  if (options.dryRun) {
@@ -1045,27 +978,26 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
1045
978
  setProcessEnv("OKE_KV_DRIVER", stackKvDriver);
1046
979
  }
1047
980
 
1048
- // One-shot schema sync for the session profile (both local and docker):
1049
- // emits schema.generated.ts for the active dialect, then pushes via
1050
- // drizzle-kit. Data planes stay isolated — session flags never rewrite
1051
- // `.oke/mode`. Watch auto-push below remains local-only.
981
+ // One-shot schema sync for the compose (`dev`) profile: emits
982
+ // schema.drizzle.ts for the active dialect, then pushes via drizzle-kit.
1052
983
  if (!options.noDbPush) {
1053
984
  const { syncDevSchema } = await import("./dev-schema-sync.ts");
985
+ const configEnv = "dev" as const;
1054
986
  try {
1055
- const result = await syncDevSchema(cwd, mode, {
987
+ const result = await syncDevSchema(cwd, configEnv, {
1056
988
  write: chromeWrite,
1057
- quietComposeReady: mode === "docker",
989
+ quietComposeReady: true,
1058
990
  });
1059
991
  write(
1060
992
  formatStatusLine(
1061
- `oke db push (${mode} · ${result.dialect}) ${result.code === 0 ? "ok" : "failed"}`,
993
+ `oke db push (dev · ${result.dialect}) ${result.code === 0 ? "ok" : "failed"}`,
1062
994
  ),
1063
995
  );
1064
996
  if (result.code === 0) {
1065
997
  const { maybeAskSeed } = await import("./ask-seed.ts");
1066
998
  await maybeAskSeed({
1067
999
  cwd,
1068
- env: mode,
1000
+ env: configEnv,
1069
1001
  write,
1070
1002
  stdinIsTTY: options.stdinIsTTY ?? process.stdin.isTTY,
1071
1003
  });
@@ -1073,7 +1005,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
1073
1005
  } catch (err) {
1074
1006
  write(
1075
1007
  formatStatusLine(
1076
- `oke db push (${mode}) skipped — ${err instanceof Error ? err.message : String(err)}`,
1008
+ `oke db push (dev) skipped — ${err instanceof Error ? err.message : String(err)}`,
1077
1009
  ),
1078
1010
  );
1079
1011
  }
@@ -1104,7 +1036,6 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
1104
1036
  OKE_KV_DRIVER: stackKvDriver,
1105
1037
  }
1106
1038
  : {}),
1107
- ...(meili?.overlay ?? {}),
1108
1039
  /** Backend child — parent Console owns process-local boot notices. */
1109
1040
  OKE_SUPPRESS_BOOT_WARN: "1",
1110
1041
  };
@@ -1252,7 +1183,10 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
1252
1183
  options.dbPush ??
1253
1184
  (async (projectCwd: string) => {
1254
1185
  const configPath = await resolveDrizzleConfigPath(projectCwd);
1255
- return runPush(configPath, (t) => write(t), { cwd: projectCwd, env: mode });
1186
+ return runPush(configPath, (t) => write(t), {
1187
+ cwd: projectCwd,
1188
+ env: "dev",
1189
+ });
1256
1190
  });
1257
1191
 
1258
1192
  let lastSchemaFilename = "";
@@ -1274,9 +1208,6 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
1274
1208
  }
1275
1209
 
1276
1210
  write(formatDevLogSeparator());
1277
- if (dockerStarted && (options.stdinIsTTY ?? process.stdin.isTTY)) {
1278
- write(formatDevControlsHint());
1279
- }
1280
1211
 
1281
1212
  const watchFs: DevWatchFn =
1282
1213
  options.watchFs ?? ((path, watchOptions, listener) => watch(path, watchOptions, listener));
@@ -1308,11 +1239,11 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
1308
1239
  bootBoard.stop();
1309
1240
  autoPushRunner.cancel();
1310
1241
  watcher.close();
1311
- meili?.stop();
1312
1242
  app.stop();
1313
1243
  consoleServer.stop();
1314
1244
  mcpServer?.stop();
1315
1245
  docsMcpServer?.stop();
1246
+ void clearDevSessionLock(cwd);
1316
1247
  if (dockerStarted) {
1317
1248
  const started = dockerStarted;
1318
1249
  dockerStarted = null;
@@ -1373,9 +1304,22 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
1373
1304
  consoleState,
1374
1305
  };
1375
1306
 
1307
+ // Record ownership so a later `oke` TUI can attach without spawning a second stack.
1308
+ await writeDevSessionLock(cwd, {
1309
+ pid: process.pid,
1310
+ ports: {
1311
+ app: boundAppPort,
1312
+ console: boundConsolePort,
1313
+ mcp: boundMcpPort,
1314
+ docsMcp: boundDocsMcpPort,
1315
+ },
1316
+ startedAt: new Date().toISOString(),
1317
+ cwd,
1318
+ });
1319
+
1376
1320
  if (options.onReady) await options.onReady(session);
1377
1321
 
1378
- // Keyboard controls (docker + TTY): start/stop/restart services without leaving oke dev.
1322
+ // Keyboard controls (docker + TTY): Ink replaces raw-mode stdin.
1379
1323
  if (keepAlive && dockerStarted && (options.stdinIsTTY ?? process.stdin.isTTY)) {
1380
1324
  const started = dockerStarted;
1381
1325
  const syncComposeBoard = async (): Promise<void> => {
@@ -1391,7 +1335,6 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
1391
1335
  };
1392
1336
  const refreshChrome = async (): Promise<void> => {
1393
1337
  await syncComposeBoard();
1394
- // Clear log noise; reprint head + latest ● board + surfaces + empty Logs.
1395
1338
  write(clearTerminalScreen());
1396
1339
  bootBoard.reset();
1397
1340
  write(
@@ -1412,48 +1355,49 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
1412
1355
  write(formatServiceLine("Docs MCP", `http://127.0.0.1:${boundDocsMcpPort}`));
1413
1356
  }
1414
1357
  write(formatDevLogSeparator());
1415
- write(formatDevControlsHint());
1416
1358
  };
1417
- const controls = startDevControls({
1418
- write,
1419
- isTTY: options.stdinIsTTY ?? process.stdin.isTTY,
1420
- onQuit: () => {
1421
- stop();
1422
- process.exit(0);
1423
- },
1424
- onRefresh: () => refreshChrome(),
1425
- // Refresh first so help never sits in the middle of request logs.
1426
- onShowPanel: async (body) => {
1427
- await refreshChrome();
1428
- write(body);
1429
- },
1430
- onComposeSettled: () => syncComposeBoard(),
1431
- composeAction: async (action: DevComposeControlAction) => {
1432
- const files = started.files;
1433
- const finalArgs =
1434
- action === "up"
1435
- ? ["compose", ...files.flatMap((f) => ["-f", f]), "up", "-d"]
1436
- : ["compose", ...files.flatMap((f) => ["-f", f]), action];
1437
- const proc = Bun.spawn(["docker", ...finalArgs], {
1438
- cwd: started.cwd,
1439
- stdout: "pipe",
1440
- stderr: "pipe",
1441
- env: { ...process.env, ...started.env },
1442
- });
1443
- const [stdout, stderr, code] = await Promise.all([
1444
- new Response(proc.stdout).text(),
1445
- new Response(proc.stderr).text(),
1446
- proc.exited,
1447
- ]);
1448
- if (code !== 0) {
1449
- const detail = [stdout.trim(), stderr.trim()].filter(Boolean).join("\n");
1450
- throw new Error(
1451
- `docker compose ${action} exited ${code}` + (detail ? `\n${detail}` : ""),
1452
- );
1453
- }
1454
- },
1455
- });
1456
- stopDevControls = () => controls.stop();
1359
+ const composeAction = async (action: DevComposeControlAction): Promise<void> => {
1360
+ const files = started.files;
1361
+ const finalArgs =
1362
+ action === "up"
1363
+ ? ["compose", ...files.flatMap((f) => ["-f", f]), "up", "-d"]
1364
+ : ["compose", ...files.flatMap((f) => ["-f", f]), action];
1365
+ const proc = Bun.spawn(["docker", ...finalArgs], {
1366
+ cwd: started.cwd,
1367
+ stdout: "pipe",
1368
+ stderr: "pipe",
1369
+ env: { ...process.env, ...started.env },
1370
+ });
1371
+ const [stdout, stderr, code] = await Promise.all([
1372
+ new Response(proc.stdout).text(),
1373
+ new Response(proc.stderr).text(),
1374
+ proc.exited,
1375
+ ]);
1376
+ if (code !== 0) {
1377
+ const detail = [stdout.trim(), stderr.trim()].filter(Boolean).join("\n");
1378
+ throw new Error(`docker compose ${action} exited ${code}` + (detail ? `\n${detail}` : ""));
1379
+ }
1380
+ await syncComposeBoard();
1381
+ };
1382
+
1383
+ try {
1384
+ const { launchDevLiveControls } = await import("./tui/DevLive.tsx");
1385
+ await launchDevLiveControls({
1386
+ onRefresh: () => refreshChrome(),
1387
+ onComposeUp: () => composeAction("up"),
1388
+ onComposeStop: () => composeAction("stop"),
1389
+ onQuit: () => {
1390
+ stop();
1391
+ process.exit(0);
1392
+ },
1393
+ });
1394
+ // Esc / unmount without q — still tear down.
1395
+ stop();
1396
+ } catch {
1397
+ // Ink optional — fall back to keep-alive without keyboard chrome.
1398
+ await new Promise(() => {});
1399
+ }
1400
+ return { code: 0, plan: session.plan, session };
1457
1401
  }
1458
1402
 
1459
1403
  if (!keepAlive) {
@@ -1465,21 +1409,6 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
1465
1409
  return { code: 0, plan: session.plan, session };
1466
1410
  }
1467
1411
 
1468
- /**
1469
- * Declared vault secret names from the project Manifest (policy scope).
1470
- *
1471
- * @param cwd - Project root
1472
- */
1473
- async function tryLoadVaultSecretNames(cwd: string): Promise<readonly string[]> {
1474
- try {
1475
- const { extractManifest } = await import("../compiler/extract.ts");
1476
- const manifest = await extractManifest({ rootDir: cwd });
1477
- return Object.keys(manifest.vault ?? {});
1478
- } catch {
1479
- return [];
1480
- }
1481
- }
1482
-
1483
1412
  /**
1484
1413
  * True when a Manifest has no declared flows (scaffold / empty extract).
1485
1414
  *
@@ -1611,20 +1540,34 @@ async function waitForAppReady(
1611
1540
  }
1612
1541
 
1613
1542
  /**
1614
- * CLI entry for `oke dev [--local|-l] [--docker|-d [roles]]`.
1543
+ * Probe `docker info` throws a clear error when the daemon is unavailable.
1544
+ */
1545
+ async function assertDockerDaemon(): Promise<void> {
1546
+ const proc = Bun.spawn(["docker", "info"], {
1547
+ stdout: "pipe",
1548
+ stderr: "pipe",
1549
+ });
1550
+ const [stderr, code] = await Promise.all([new Response(proc.stderr).text(), proc.exited]);
1551
+ if (code === 0) return;
1552
+ const detail = stderr.trim();
1553
+ throw new Error(
1554
+ "oke dev: Docker daemon unavailable — start Docker Desktop (or the Docker daemon) and retry" +
1555
+ (detail ? `\n${detail}` : ""),
1556
+ );
1557
+ }
1558
+
1559
+ /**
1560
+ * CLI entry for `oke dev [-d [roles]]`.
1615
1561
  *
1616
1562
  * @param args - Args after `dev`
1617
1563
  */
1618
1564
  export async function devCli(args: readonly string[]): Promise<number> {
1619
1565
  let docker: boolean | string[] | undefined;
1620
- let local = false;
1621
1566
  let noDbPush = false;
1622
1567
  let entry: string | undefined;
1623
1568
  for (let i = 0; i < args.length; i++) {
1624
1569
  const a = args[i]!;
1625
- if (a === "--local" || a === "-l") {
1626
- local = true;
1627
- } else if (a === "--docker" || a === "-d") {
1570
+ if (a === "--docker" || a === "-d") {
1628
1571
  const next = args[i + 1];
1629
1572
  if (next && !next.startsWith("-") && /^[\w.,]+$/.test(next)) {
1630
1573
  docker = next
@@ -1639,23 +1582,26 @@ export async function devCli(args: readonly string[]): Promise<number> {
1639
1582
  noDbPush = true;
1640
1583
  } else if (a === "--entry" || a === "-e") entry = args[++i];
1641
1584
  else if (a === "--help" || a === "-h") {
1642
- console.log(`oke dev [--local|-l] [--docker|-d [roles]] [--no-db-push] [--entry|-e src/app.ts]
1585
+ console.log(`oke dev [-d [roles]] [--no-db-push] [--entry|-e src/app.ts]
1643
1586
 
1644
1587
  Watch · hot reload · Console :6533 · app :6530 · MCP :6535
1645
- Regenerates client types on every save.
1646
- Local mode auto-runs \`oke db push\` when schema.ts changes (opt out: --no-db-push).
1647
-
1648
- Bare \`oke dev\` uses .oke/mode (one-time prompt on a TTY; non-TTY → local).
1649
- --local / --docker override for this session only (never write .oke/mode).
1650
- --docker boots infra under docker/ and runs the app on host Bun with prod
1651
- store drivers (Postgres/Redis). Partial roles: -d store.sql,store.kv.
1652
- Change the saved default with \`oke mode local|docker\`.
1588
+ Always starts Docker Compose under docker/ and runs the app on host Bun
1589
+ with compose store drivers (Postgres/Redis). Regenerates client types on
1590
+ every save. Auto-runs \`oke db push\` when schema.ts changes (opt out:
1591
+ --no-db-push).
1592
+
1593
+ Optional \`-d\` / \`--docker\` filters which compose roles to start
1594
+ (e.g. \`-d store.sql,store.kv\`). Requires a running Docker daemon.
1595
+ Use \`oke test\` for PGLite / in-memory test posture (no Compose).
1653
1596
  `);
1654
1597
  return 0;
1598
+ } else if (a === "--local" || a === "-l") {
1599
+ console.error("oke dev: --local was removed — `oke dev` always uses Docker Compose");
1600
+ console.error("Use `oke test` for PGLite test posture.");
1601
+ return 1;
1655
1602
  }
1656
1603
  }
1657
1604
  const { code } = await runDev({
1658
- ...(local ? { local: true } : {}),
1659
1605
  ...(docker === undefined ? {} : { docker }),
1660
1606
  ...(noDbPush ? { noDbPush: true } : {}),
1661
1607
  entry,
@@ -11,9 +11,7 @@ import { join } from "node:path";
11
11
 
12
12
  const ROOT = join(import.meta.dir, "../..");
13
13
 
14
- /** Forbidden CLI flag, split so this file does not match itself. */
15
- const FORBIDDEN_STACK_FLAG = ["--", "stack"].join("");
16
- /** Forbidden compose env filename. */
14
+ /** Forbidden compose env filename (legacy soft-compat). */
17
15
  const FORBIDDEN_ENV_STACK = [".env.", "stack"].join("");
18
16
  /** Forbidden env var — name avoids the contiguous token so this file is clean. */
19
17
  const FORBIDDEN_LEGACY_ENV = ["OKE_", "STACK"].join("");
@@ -47,9 +45,8 @@ function assertZeroGitGrep(pattern: string, ignorePrefixes: readonly string[] =
47
45
  }
48
46
 
49
47
  describe("legacy stack-mode removal gate", () => {
50
- test("tracked tree has zero legacy docker-mode flag outside changelog", () => {
51
- assertZeroGitGrep(FORBIDDEN_STACK_FLAG, ["changelog.md:"]);
52
- });
48
+ // `--stack` is first-class again (Swarm `docker-stack.yml` layout). Legacy
49
+ // soft-compat was the env filename + vault helper surface — those stay forbidden.
53
50
 
54
51
  test("tracked tree has zero legacy compose env filename outside changelog", () => {
55
52
  assertZeroGitGrep(FORBIDDEN_ENV_STACK, ["changelog.md:"]);
@@ -8,7 +8,12 @@ import { tmpdir } from "node:os";
8
8
  import { join } from "node:path";
9
9
  import { composeProjectName, type DockerRunner } from "../docker/cleanup.ts";
10
10
  import { dockerCli } from "./docker.ts";
11
- import { dockerCleanHelp, parseDockerCleanArgs, runDockerClean } from "./docker-clean.ts";
11
+ import {
12
+ dockerCleanHelp,
13
+ parseDockerCleanArgs,
14
+ runDockerClean,
15
+ selectionEntries,
16
+ } from "./docker-clean.ts";
12
17
  import { EXIT_OK, EXIT_USAGE } from "./exit.ts";
13
18
  import { formatOkeHelp, flagTokensFor, OKE_COMMANDS } from "./registry.ts";
14
19
 
@@ -176,4 +181,31 @@ describe("oke docker clean", () => {
176
181
  expect(flagTokensFor(docker, "clean")).toContain("--all");
177
182
  expect(formatOkeHelp()).toContain("clean");
178
183
  });
184
+
185
+ test("selectionEntries is one row per project (not per container)", () => {
186
+ const entries = selectionEntries([
187
+ {
188
+ project: "oke-dev-aaaaaa",
189
+ containers: [
190
+ {
191
+ id: "1",
192
+ name: "oke-dev-aaaaaa-redis-1",
193
+ service: "redis",
194
+ state: "running",
195
+ project: "oke-dev-aaaaaa",
196
+ },
197
+ {
198
+ id: "2",
199
+ name: "oke-dev-aaaaaa-postgres-1",
200
+ service: "postgres",
201
+ state: "running",
202
+ project: "oke-dev-aaaaaa",
203
+ },
204
+ ],
205
+ },
206
+ ]);
207
+ expect(entries).toHaveLength(1);
208
+ expect(entries[0]?.label).toBe("oke-dev-aaaaaa");
209
+ expect(entries[0]?.hint).toContain("2 container");
210
+ });
179
211
  });