okengine 0.2.8 → 0.3.4

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 (180) hide show
  1. package/AGENTS.md +17 -15
  2. package/README.md +59 -31
  3. package/package.json +11 -16
  4. package/site/content/docs/ai/llms-txt.mdx +54 -0
  5. package/site/content/docs/ai/mcp.mdx +123 -0
  6. package/site/content/docs/ai/meta.json +5 -0
  7. package/site/content/docs/ai/skills.mdx +53 -0
  8. package/site/content/docs/console/access.mdx +29 -0
  9. package/site/content/docs/console/ai.mdx +35 -0
  10. package/site/content/docs/console/architecture.mdx +35 -0
  11. package/site/content/docs/console/channels.mdx +37 -0
  12. package/site/content/docs/console/clock.mdx +31 -0
  13. package/site/content/docs/console/flows.mdx +31 -0
  14. package/site/content/docs/console/gates.mdx +35 -0
  15. package/site/content/docs/console/manifest-diff.mdx +34 -0
  16. package/site/content/docs/console/meta.json +23 -0
  17. package/site/content/docs/console/overview.mdx +40 -0
  18. package/site/content/docs/console/plugins.mdx +41 -0
  19. package/site/content/docs/console/privacy.mdx +32 -0
  20. package/site/content/docs/console/runs.mdx +40 -0
  21. package/site/content/docs/console/signals.mdx +31 -0
  22. package/site/content/docs/console/store.mdx +32 -0
  23. package/site/content/docs/console/tenancy.mdx +32 -0
  24. package/site/content/docs/console/traces.mdx +34 -0
  25. package/site/content/docs/console/vault.mdx +37 -0
  26. package/site/content/docs/elements/ai.mdx +180 -0
  27. package/site/content/docs/elements/channel.mdx +167 -0
  28. package/site/content/docs/elements/clock.mdx +182 -0
  29. package/site/content/docs/elements/flow.mdx +288 -0
  30. package/site/content/docs/elements/gate.mdx +171 -0
  31. package/site/content/docs/elements/meta.json +5 -0
  32. package/site/content/docs/elements/signal.mdx +171 -0
  33. package/site/content/docs/elements/store.mdx +320 -0
  34. package/site/content/docs/elements/vault.mdx +263 -0
  35. package/site/content/docs/get-started/basic-usage.mdx +124 -0
  36. package/site/content/docs/get-started/comparison.mdx +65 -0
  37. package/site/content/docs/get-started/installation.mdx +113 -0
  38. package/site/content/docs/get-started/introduction.mdx +123 -0
  39. package/site/content/docs/get-started/meta.json +5 -0
  40. package/site/content/docs/index.mdx +63 -0
  41. package/site/content/docs/meta.json +5 -0
  42. package/site/content/docs/plugins/compression.mdx +60 -0
  43. package/site/content/docs/plugins/cors.mdx +98 -0
  44. package/site/content/docs/plugins/csrf.mdx +96 -0
  45. package/site/content/docs/plugins/ip-allowlist.mdx +93 -0
  46. package/site/content/docs/plugins/maintenance-mode.mdx +101 -0
  47. package/site/content/docs/plugins/meta.json +15 -0
  48. package/site/content/docs/plugins/security-headers.mdx +136 -0
  49. package/site/content/docs/reference/cli.md +101 -0
  50. package/site/content/docs/reference/configuration.mdx +159 -0
  51. package/site/content/docs/reference/environment-variables.mdx +87 -0
  52. package/site/content/docs/reference/errors.mdx +80 -0
  53. package/site/content/docs/reference/fx.mdx +117 -0
  54. package/site/content/docs/reference/meta.json +5 -0
  55. package/site/content/docs/reference/plugins.mdx +249 -0
  56. package/site/content/docs/reference/security.md +63 -0
  57. package/src/auth/auth.test.ts +3 -0
  58. package/src/cli/ask-dev-mode.ts +1 -1
  59. package/src/cli/db.ts +87 -17
  60. package/src/cli/dev-db-push.test.ts +32 -2
  61. package/src/cli/dev-schema-sync.test.ts +66 -0
  62. package/src/cli/dev-schema-sync.ts +139 -0
  63. package/src/cli/dev.test.ts +123 -1
  64. package/src/cli/dev.ts +150 -8
  65. package/src/cli/doc-staleness.test.ts +4 -4
  66. package/src/cli/docker-cli.test.ts +20 -0
  67. package/src/cli/docker.ts +10 -0
  68. package/src/cli/drizzle-env.test.ts +67 -0
  69. package/src/cli/drizzle-env.ts +78 -0
  70. package/src/cli/ensure-drizzle-config.ts +50 -0
  71. package/src/cli/hero-meta.test.ts +1 -1
  72. package/src/cli/load-config.ts +6 -0
  73. package/src/cli/mode.ts +24 -4
  74. package/src/cli/openbao-bootstrap.test.ts +149 -0
  75. package/src/cli/openbao-bootstrap.ts +314 -0
  76. package/src/cli/openbao-restart.integration.test.ts +136 -0
  77. package/src/cli/ports.test.ts +7 -5
  78. package/src/cli/ports.ts +6 -2
  79. package/src/cli/resolve-dev-sql-env.test.ts +48 -0
  80. package/src/cli/resolve-dev-sql-env.ts +42 -0
  81. package/src/cli/stack.ts +7 -4
  82. package/src/cli/vault-cmd.ts +63 -0
  83. package/src/client/types.ts +7 -1
  84. package/src/compiler/extract.test.ts +40 -0
  85. package/src/compiler/extract.ts +123 -1
  86. package/src/compiler/fixtures/skyport/oke.config.ts +2 -2
  87. package/src/compiler/fixtures/skyport.expected.json +1 -1
  88. package/src/compiler/response.ts +12 -0
  89. package/src/config/define-config.test.ts +4 -6
  90. package/src/config/index.ts +2 -15
  91. package/src/console/server/app.ts +2 -0
  92. package/src/console/server/vault.ts +21 -6
  93. package/src/docker/compose.ts +112 -16
  94. package/src/docker/derive.ts +7 -1
  95. package/src/docker/docker.test.ts +103 -0
  96. package/src/docker/index.ts +11 -1
  97. package/src/docker/recipes/index.ts +3 -2
  98. package/src/docker/recipes/openbao.ts +47 -0
  99. package/src/docker/recipes/redis.ts +5 -1
  100. package/src/docker/recipes/rustfs.ts +2 -3
  101. package/src/docker/stack-id.test.ts +43 -8
  102. package/src/docker/stack-id.ts +99 -20
  103. package/src/docker/stack.ts +36 -4
  104. package/src/docker/types.ts +3 -0
  105. package/src/docs-origin.ts +4 -4
  106. package/src/drivers/drizzle-dialect.test.ts +20 -0
  107. package/src/drivers/drizzle-dialect.ts +37 -0
  108. package/src/drivers/index.ts +1 -2
  109. package/src/drivers/memory.ts +278 -39
  110. package/src/drivers/s3.ts +10 -1
  111. package/src/drivers/vault-driver-removal.test.ts +55 -0
  112. package/src/drivers/vault-openbao.test.ts +97 -0
  113. package/src/drivers/vault-openbao.ts +102 -35
  114. package/src/drivers/vault-types.ts +3 -10
  115. package/src/elements/store/declare.ts +4 -1
  116. package/src/elements/store/resource-list-docs.fixture.ts +56 -0
  117. package/src/elements/store/resource-list-docs.test.ts +79 -0
  118. package/src/elements/store/resource.test.ts +253 -0
  119. package/src/elements/store/resource.ts +786 -0
  120. package/src/elements/store/sql-condition.test.ts +132 -0
  121. package/src/elements/store/sql-condition.ts +284 -46
  122. package/src/elements/store/sql-session.test.ts +86 -1
  123. package/src/elements/store/sql-session.ts +187 -27
  124. package/src/elements/store/table.ts +34 -4
  125. package/src/elements/store.ts +16 -0
  126. package/src/elements/vault/runtime.ts +1 -1
  127. package/src/elements/vault.test.ts +1 -28
  128. package/src/elements/vault.ts +1 -1
  129. package/src/kernel/app.ts +59 -25
  130. package/src/kernel/boot-bind/channel.test.ts +60 -0
  131. package/src/kernel/boot-bind/channel.ts +64 -2
  132. package/src/kernel/boot-bind/store.test.ts +10 -1
  133. package/src/kernel/boot-bind/store.ts +49 -2
  134. package/src/kernel/boot.test.ts +0 -1
  135. package/src/kernel/boot.ts +1 -1
  136. package/src/kernel/edge.test.ts +68 -0
  137. package/src/kernel/errors.registry.test.ts +1 -1
  138. package/src/kernel/flow.ts +8 -0
  139. package/src/kernel/fx.test.ts +23 -3
  140. package/src/kernel/fx.ts +115 -18
  141. package/src/kernel/hooks.test.ts +33 -0
  142. package/src/kernel/hooks.ts +22 -0
  143. package/src/kernel/index.ts +7 -0
  144. package/src/kernel/on.ts +44 -3
  145. package/src/kernel/plugin.ts +33 -3
  146. package/src/kernel/registry-isolation.test.ts +74 -0
  147. package/src/kernel/registry.ts +22 -1
  148. package/src/kernel/triggers.ts +59 -0
  149. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  150. package/src/manifest/fixtures/skyport.manifest.json +1 -1
  151. package/src/manifest/index.ts +1 -1
  152. package/src/manifest/types.ts +1 -1
  153. package/src/manifest/validate.ts +2 -2
  154. package/src/plugins/compression.test.ts +127 -0
  155. package/src/plugins/compression.ts +94 -0
  156. package/src/plugins/config-source.test.ts +204 -0
  157. package/src/plugins/config-source.ts +209 -0
  158. package/src/plugins/cors.test.ts +172 -0
  159. package/src/plugins/cors.ts +153 -0
  160. package/src/plugins/csrf.test.ts +102 -0
  161. package/src/plugins/csrf.ts +86 -0
  162. package/src/plugins/headers.ts +54 -0
  163. package/src/plugins/index.ts +26 -0
  164. package/src/plugins/ip-allowlist.test.ts +139 -0
  165. package/src/plugins/ip-allowlist.ts +129 -0
  166. package/src/plugins/maintenance-mode.test.ts +91 -0
  167. package/src/plugins/maintenance-mode.ts +85 -0
  168. package/src/plugins/security-headers.test.ts +243 -0
  169. package/src/plugins/security-headers.ts +255 -0
  170. package/src/release/measure.ts +1 -2
  171. package/src/test/create-test-app.ts +14 -2
  172. package/docs/spec/console.md +0 -762
  173. package/docs/spec/example.md +0 -1374
  174. package/docs/spec/four-applications.md +0 -1376
  175. package/docs/spec/unified-theory.md +0 -498
  176. package/src/cli/doc-drift.test.ts +0 -147
  177. package/src/cli/doc-drift.ts +0 -401
  178. package/src/cli/doctor-diff-examples.ts +0 -90
  179. package/src/drivers/vault-sops.ts +0 -246
  180. /package/{spec/manifest.v1.schema.json → manifest.v1.schema.json} +0 -0
package/src/cli/dev.ts CHANGED
@@ -13,7 +13,9 @@ import type { ConsoleState } from "../console/server/state.ts";
13
13
  import {
14
14
  DEFAULT_DOCKER_DIR,
15
15
  deriveInfrastructure,
16
+ loadExistingStackControls,
16
17
  loadExistingStackCredentials,
18
+ resolveExtraPorts,
17
19
  stackAppSlug,
18
20
  stackInstanceId,
19
21
  writeDerivedFiles,
@@ -21,7 +23,7 @@ import {
21
23
  } from "../docker/index.ts";
22
24
  import type { Manifest } from "../manifest/types.ts";
23
25
  import type { McpContext } from "../mcp/tools.ts";
24
- import { APP_PORT, CONSOLE_PORT, MCP_PORT } from "../runtime/types.ts";
26
+ import { APP_PORT, CONSOLE_PORT, DOCS_MCP_PORT, MCP_PORT } from "../runtime/types.ts";
25
27
  import {
26
28
  formatDevBanner,
27
29
  formatDevLogSeparator,
@@ -90,6 +92,12 @@ export interface DevServeMcpOptions {
90
92
  readonly hostname?: string;
91
93
  }
92
94
 
95
+ /** Options for {@link DevOptions.serveDocsMcp}. */
96
+ export interface DevServeDocsMcpOptions {
97
+ readonly port: number;
98
+ readonly hostname?: string;
99
+ }
100
+
93
101
  /** Live `oke dev` session — returned when {@link DevOptions.keepAlive} is false. */
94
102
  export interface DevSession {
95
103
  readonly plan: DevPlan;
@@ -99,6 +107,10 @@ export interface DevSession {
99
107
  readonly consolePort: number;
100
108
  readonly mcpPort: number;
101
109
  readonly mcpUrl: URL | null;
110
+ /** Docs MCP listen port (read-only, unauthenticated). */
111
+ readonly docsMcpPort: number;
112
+ /** Docs MCP base URL — null when the surface was skipped. */
113
+ readonly docsMcpUrl: URL | null;
102
114
  /** Shared with Console — mint `oke-mcp` tokens against this store/secret. */
103
115
  readonly secret: string | null;
104
116
  readonly sessions: SessionStore | null;
@@ -106,6 +118,13 @@ export interface DevSession {
106
118
  readonly consoleState: ConsoleState | null;
107
119
  }
108
120
 
121
+ /** Minimal filesystem watcher contract, injectable for deterministic tests. */
122
+ export type DevWatchFn = (
123
+ path: string,
124
+ options: { readonly recursive: true },
125
+ listener: (event: "rename" | "change", filename: string | null) => void,
126
+ ) => { close(): void };
127
+
109
128
  /** Options for {@link runDev}. */
110
129
  export interface DevOptions {
111
130
  readonly cwd?: string;
@@ -134,6 +153,8 @@ export interface DevOptions {
134
153
  * @param filename - Relative path from the watcher
135
154
  */
136
155
  readonly onDbAutoPush?: (filename: string) => void;
156
+ /** Watch project source changes (injectable for tests). */
157
+ readonly watchFs?: DevWatchFn;
137
158
  readonly images?: Readonly<Record<string, string>>;
138
159
  readonly credentials?: DeriveOptions["credentials"];
139
160
  readonly write?: (text: string) => void;
@@ -156,6 +177,7 @@ export interface DevOptions {
156
177
  readonly appPort?: number;
157
178
  readonly consolePort?: number;
158
179
  readonly mcpPort?: number;
180
+ readonly docsMcpPort?: number;
159
181
  /**
160
182
  * Boot compose (injectable).
161
183
  *
@@ -176,6 +198,12 @@ export interface DevOptions {
176
198
  * Default: {@link serveMcp} on :6535.
177
199
  */
178
200
  readonly serveMcp?: (options: DevServeMcpOptions) => Promise<DevMcpHandle>;
201
+ /**
202
+ * Serve the read-only docs MCP (injectable).
203
+ * Default: {@link serveDocsMcp} on :6536. Boot failure skips the surface —
204
+ * docs search must never take `oke dev` down.
205
+ */
206
+ readonly serveDocsMcp?: (options: DevServeDocsMcpOptions) => Promise<DevMcpHandle>;
179
207
  /**
180
208
  * Boot + serve the app entry (injectable).
181
209
  * Default: spawn `bun --hot` on the shipped {@link ./dev-app-runner.ts}
@@ -199,6 +227,7 @@ export interface DevPlan {
199
227
  readonly appPort: number;
200
228
  readonly consolePort: number;
201
229
  readonly mcpPort: number;
230
+ readonly docsMcpPort: number;
202
231
  readonly stackRoles: readonly string[] | null;
203
232
  readonly composeFiles: readonly string[] | null;
204
233
  readonly stackEnv: Readonly<Record<string, string>> | null;
@@ -223,24 +252,29 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
223
252
  const preferredApp = options.appPort ?? Number(Bun.env.PORT ?? APP_PORT);
224
253
  const preferredConsole = options.consolePort ?? CONSOLE_PORT;
225
254
  const preferredMcp = options.mcpPort ?? MCP_PORT;
255
+ const preferredDocsMcp = options.docsMcpPort ?? DOCS_MCP_PORT;
226
256
  // Explicit overrides (incl. `0` ephemeral) skip probing; otherwise +1 until free.
227
257
  const ports =
228
258
  options.appPort !== undefined ||
229
259
  options.consolePort !== undefined ||
230
- options.mcpPort !== undefined
260
+ options.mcpPort !== undefined ||
261
+ options.docsMcpPort !== undefined
231
262
  ? {
232
263
  app: preferredApp,
233
264
  console: preferredConsole,
234
265
  mcp: preferredMcp,
266
+ docsMcp: preferredDocsMcp,
235
267
  }
236
268
  : await resolveDevPorts({
237
269
  app: preferredApp,
238
270
  console: preferredConsole,
239
271
  mcp: preferredMcp,
272
+ docsMcp: preferredDocsMcp,
240
273
  });
241
274
  const appPort = ports.app;
242
275
  const consolePort = ports.console;
243
276
  const mcpPort = ports.mcp;
277
+ const docsMcpPort = ports.docsMcp;
244
278
  const keepAlive = options.keepAlive ?? true;
245
279
 
246
280
  let entry = options.entry;
@@ -344,6 +378,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
344
378
  const appSlug = stackAppSlug(cwd);
345
379
  const reusedCreds =
346
380
  options.credentials ?? (await loadExistingStackCredentials(cwd, stackRoles));
381
+ const controls = await loadExistingStackControls(cwd);
347
382
  const derived = deriveInfrastructure({
348
383
  images,
349
384
  app: appSlug,
@@ -352,6 +387,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
352
387
  composeDir,
353
388
  instanceId,
354
389
  ...(reusedCreds ? { credentials: reusedCreds } : {}),
390
+ ...(controls ? { controls } : {}),
355
391
  host: "127.0.0.1",
356
392
  });
357
393
  // Layer 4 is user-owned — include only if the file already exists.
@@ -383,6 +419,27 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
383
419
  });
384
420
  // Compose files live under docker/; cwd for compose is that directory.
385
421
  await up(composeFiles, dockerOut);
422
+
423
+ // Real OpenBao vault: init once / unseal every start, then expose the
424
+ // least-privilege app token to the app + Console (never root).
425
+ const vaultSpec = derived.specs.find((s) => s.role === "vault");
426
+ if (vaultSpec) {
427
+ const { ensureOpenBao, openbaoStackEnv } = await import("./openbao-bootstrap.ts");
428
+ const vaultNames = await tryLoadVaultSecretNames(cwd);
429
+ const boot = await ensureOpenBao({
430
+ cwd,
431
+ url: `http://127.0.0.1:${vaultSpec.hostPort}`,
432
+ names: vaultNames,
433
+ });
434
+ Object.assign(stackEnv!, openbaoStackEnv(boot));
435
+ const envPath = resolve(dockerOut, ".env.docker");
436
+ const current = await Bun.file(envPath).text();
437
+ const tokenLine = `OKE_VAULT_TOKEN=${boot.appToken}`;
438
+ const next = /OKE_VAULT_TOKEN=.*/.test(current)
439
+ ? current.replace(/OKE_VAULT_TOKEN=.*/, tokenLine)
440
+ : `${current.trimEnd()}\nOKE_VAULT_TOKEN=${boot.appToken}\nOKE_VAULT_MOUNT=secret\n`;
441
+ await Bun.write(envPath, next);
442
+ }
386
443
  } catch (err) {
387
444
  const msg = err instanceof Error ? err.message : String(err);
388
445
  console.error(msg);
@@ -392,8 +449,15 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
392
449
  const roleLabel = (role: string): string => {
393
450
  if (role === "store.sql") return "postgres";
394
451
  if (role === "store.kv") return "redis";
452
+ if (role === "store.files") return "files";
453
+ if (role === "channel.email") return "mail";
395
454
  return role;
396
455
  };
456
+ const extraLabel = (role: string, containerPort: number): string => {
457
+ if (role === "channel.email" && containerPort === 8025) return "mail-ui";
458
+ if (role === "store.files" && containerPort === 9001) return "files-ui";
459
+ return `${roleLabel(role)}+${containerPort}`;
460
+ };
397
461
  const appDrivers = [
398
462
  ...(stackRoles.includes("store.sql") ? [stackSqlDriver] : []),
399
463
  ...(stackRoles.includes("store.kv") ? [stackKvDriver] : []),
@@ -401,10 +465,13 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
401
465
  write(
402
466
  formatStackSummary({
403
467
  project: `oke-${appSlug}`,
404
- services: derived.specs.map((spec) => ({
405
- label: roleLabel(spec.role),
406
- hostPort: spec.hostPort,
407
- })),
468
+ services: derived.specs.flatMap((spec) => [
469
+ { label: roleLabel(spec.role), hostPort: spec.hostPort },
470
+ ...resolveExtraPorts(spec, { images, instanceId }).map((p) => ({
471
+ label: extraLabel(spec.role, p.containerPort),
472
+ hostPort: p.hostPort,
473
+ })),
474
+ ]),
408
475
  appDrivers,
409
476
  }),
410
477
  );
@@ -416,6 +483,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
416
483
  appPort,
417
484
  consolePort,
418
485
  mcpPort,
486
+ docsMcpPort,
419
487
  stackRoles,
420
488
  composeFiles,
421
489
  stackEnv,
@@ -452,6 +520,28 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
452
520
 
453
521
  if (options.dryRun) return { code: 0, plan };
454
522
 
523
+ // One-shot schema sync for the session profile (both local and docker):
524
+ // emits schema.generated.ts for the active dialect, then pushes via
525
+ // drizzle-kit. Data planes stay isolated — session flags never rewrite
526
+ // `.oke/mode`. Watch auto-push below remains local-only.
527
+ if (!options.noDbPush) {
528
+ const { syncDevSchema } = await import("./dev-schema-sync.ts");
529
+ try {
530
+ const result = await syncDevSchema(cwd, mode, { write });
531
+ write(
532
+ formatStatusLine(
533
+ `oke db push (${mode} · ${result.dialect}) ${result.code === 0 ? "ok" : "failed"}`,
534
+ ),
535
+ );
536
+ } catch (err) {
537
+ write(
538
+ formatStatusLine(
539
+ `oke db push (${mode}) skipped — ${err instanceof Error ? err.message : String(err)}`,
540
+ ),
541
+ );
542
+ }
543
+ }
544
+
455
545
  const autoPushEnabled = resolveDevAutoPush({
456
546
  noDbPush: options.noDbPush,
457
547
  docker: Boolean(stackEnv),
@@ -542,6 +632,24 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
542
632
  };
543
633
  });
544
634
 
635
+ const serveDocsMcpSurface =
636
+ options.serveDocsMcp ??
637
+ (async (docsOptions) => {
638
+ const { serveDocsMcp } = await import("../mcp/docs-server.ts");
639
+ const server = await serveDocsMcp({
640
+ port: docsOptions.port,
641
+ hostname: docsOptions.hostname ?? "127.0.0.1",
642
+ });
643
+ write(formatServiceLine("Docs MCP", `http://127.0.0.1:${server.port}`));
644
+ return {
645
+ port: server.port,
646
+ url: server.url,
647
+ stop() {
648
+ server.stop(true);
649
+ },
650
+ };
651
+ });
652
+
545
653
  const regen =
546
654
  options.regenClient ??
547
655
  (async (appUrl) => {
@@ -581,8 +689,21 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
581
689
  });
582
690
  }
583
691
 
692
+ // Docs MCP is independent of Console state — read-only, no sessions/secret.
693
+ // Boot failure (missing docs content, busy port) skips it, never kills dev.
694
+ let docsMcpServer: DevMcpHandle | null = null;
695
+ try {
696
+ docsMcpServer = await serveDocsMcpSurface({ port: docsMcpPort, hostname: "127.0.0.1" });
697
+ } catch (err) {
698
+ write(
699
+ formatStatusLine(`Docs MCP skipped — ${err instanceof Error ? err.message : String(err)}`),
700
+ );
701
+ }
702
+
584
703
  const boundMcpPort = mcpServer?.port ?? mcpPort;
585
704
  const mcpUrl = mcpServer?.url ?? null;
705
+ const boundDocsMcpPort = docsMcpServer?.port ?? docsMcpPort;
706
+ const docsMcpUrl = docsMcpServer?.url ?? null;
586
707
 
587
708
  const appUrl = app.url?.origin ?? `http://127.0.0.1:${boundAppPort}`;
588
709
  await regen(appUrl);
@@ -591,7 +712,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
591
712
  options.dbPush ??
592
713
  (async (projectCwd: string) => {
593
714
  const configPath = await resolveDrizzleConfigPath(projectCwd);
594
- return runPush(configPath, (t) => write(t));
715
+ return runPush(configPath, (t) => write(t), { cwd: projectCwd, env: mode });
595
716
  });
596
717
 
597
718
  let lastSchemaFilename = "";
@@ -614,7 +735,9 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
614
735
 
615
736
  write(formatDevLogSeparator());
616
737
 
617
- const watcher = watch(resolve(cwd, "src"), { recursive: true }, (_event, filename) => {
738
+ const watchFs: DevWatchFn =
739
+ options.watchFs ?? ((path, watchOptions, listener) => watch(path, watchOptions, listener));
740
+ const watcher = watchFs(resolve(cwd, "src"), { recursive: true }, (_event, filename) => {
618
741
  void regen(appUrl);
619
742
  // Only live-extract when the host did not pin a Manifest (tests).
620
743
  if (options.manifest === undefined) {
@@ -635,6 +758,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
635
758
  app.stop();
636
759
  consoleServer.stop();
637
760
  mcpServer?.stop();
761
+ docsMcpServer?.stop();
638
762
  process.off("SIGINT", onSignal);
639
763
  process.off("SIGTERM", onSignal);
640
764
  };
@@ -652,12 +776,15 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
652
776
  appPort: boundAppPort,
653
777
  consolePort: boundConsolePort,
654
778
  mcpPort: boundMcpPort,
779
+ docsMcpPort: boundDocsMcpPort,
655
780
  },
656
781
  stop,
657
782
  appPort: boundAppPort,
658
783
  consolePort: boundConsolePort,
659
784
  mcpPort: boundMcpPort,
660
785
  mcpUrl,
786
+ docsMcpPort: boundDocsMcpPort,
787
+ docsMcpUrl,
661
788
  secret,
662
789
  sessions,
663
790
  consoleState,
@@ -674,6 +801,21 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
674
801
  return { code: 0, plan: session.plan, session };
675
802
  }
676
803
 
804
+ /**
805
+ * Declared vault secret names from the project Manifest (policy scope).
806
+ *
807
+ * @param cwd - Project root
808
+ */
809
+ async function tryLoadVaultSecretNames(cwd: string): Promise<readonly string[]> {
810
+ try {
811
+ const { extractManifest } = await import("../compiler/extract.ts");
812
+ const manifest = await extractManifest({ rootDir: cwd });
813
+ return Object.keys(manifest.vault ?? {});
814
+ } catch {
815
+ return [];
816
+ }
817
+ }
818
+
677
819
  /**
678
820
  * True when a Manifest has no declared flows (scaffold / empty extract).
679
821
  *
@@ -48,20 +48,20 @@ function assertZeroGitGrep(pattern: string, ignorePrefixes: readonly string[] =
48
48
 
49
49
  describe("legacy stack-mode removal gate", () => {
50
50
  test("tracked tree has zero legacy docker-mode flag outside changelog", () => {
51
- assertZeroGitGrep(FORBIDDEN_STACK_FLAG, ["docs/changelog.md:"]);
51
+ assertZeroGitGrep(FORBIDDEN_STACK_FLAG, ["changelog.md:"]);
52
52
  });
53
53
 
54
54
  test("tracked tree has zero legacy compose env filename outside changelog", () => {
55
- assertZeroGitGrep(FORBIDDEN_ENV_STACK, ["docs/changelog.md:"]);
55
+ assertZeroGitGrep(FORBIDDEN_ENV_STACK, ["changelog.md:"]);
56
56
  });
57
57
 
58
58
  test("tracked tree has zero legacy OKE env outside changelog", () => {
59
- assertZeroGitGrep(FORBIDDEN_LEGACY_ENV, ["docs/changelog.md:"]);
59
+ assertZeroGitGrep(FORBIDDEN_LEGACY_ENV, ["changelog.md:"]);
60
60
  });
61
61
 
62
62
  test("tracked tree has zero legacy vault helper outside changelog + upgrade", () => {
63
63
  assertZeroGitGrep(FORBIDDEN_FROM_STACK, [
64
- "docs/changelog.md:",
64
+ "changelog.md:",
65
65
  "src/upgrade/codemods.ts:",
66
66
  "src/upgrade/codemods.test.ts:",
67
67
  ]);
@@ -63,6 +63,26 @@ describe("oke stack", () => {
63
63
  expect(out).toContain("postgres");
64
64
  expect(await Bun.file(join(dir, "compose.yml")).exists()).toBe(false);
65
65
  });
66
+
67
+ test("uses per-project host ports (not raw recipe defaults)", async () => {
68
+ const { stackInstanceId, hostPortForInstance } = await import("../docker/index.ts");
69
+ const dir = await mkdtemp(join(tmpdir(), "oke-cli-stack-ports-"));
70
+ let out = "";
71
+ const code = await runStackPreview({
72
+ cwd: dir,
73
+ images: {
74
+ "channel.email": "axllent/mailpit:v1.22.3",
75
+ },
76
+ write: (t) => {
77
+ out += t;
78
+ },
79
+ });
80
+ expect(code).toBe(0);
81
+ const id = stackInstanceId(dir);
82
+ const smtp = hostPortForInstance("channel.email", 1025, id);
83
+ expect(out).toContain(`${smtp}:1025`);
84
+ expect(out).not.toContain("1025:1025");
85
+ });
66
86
  });
67
87
 
68
88
  describe("oke images pin", () => {
package/src/cli/docker.ts CHANGED
@@ -88,6 +88,16 @@ export async function runDockerDerive(
88
88
  await writeDerivedFiles(result, outDir, { writeStackEnv: false });
89
89
  }
90
90
 
91
+ const vaultSpec = result.specs.find((s) => s.role === "vault");
92
+ if (vaultSpec) {
93
+ const rel = composeDir === "." ? ".oke/openbao/" : `${composeDir}/../.oke/openbao/`;
94
+ write(
95
+ `oke docker: OpenBao first boot — bring the stack up (e.g. \`oke dev -d\`), and oke ` +
96
+ `will initialize + unseal it once. Root/unseal material stays on the host at ${rel} ` +
97
+ `(0600, gitignored) — back it up; losing it loses every secret permanently.\n`,
98
+ );
99
+ }
100
+
91
101
  write(`oke docker: wrote ${result.files.length} file(s) → ${composeDir}/\n`);
92
102
  for (const f of result.files) write(` ${composeDir}/${f.path}\n`);
93
103
  write(
@@ -0,0 +1,67 @@
1
+ /**
2
+ * drizzle-kit env resolution — dialect from configured driver, not URL sniffing.
3
+ */
4
+
5
+ import { afterEach, describe, expect, test } from "bun:test";
6
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import type { OkeConfig } from "../config/index.ts";
10
+ import { resolveDrizzleKitEnv } from "./drizzle-env.ts";
11
+
12
+ const dirs: string[] = [];
13
+
14
+ afterEach(async () => {
15
+ await Promise.all(dirs.splice(0).map((d) => rm(d, { recursive: true, force: true })));
16
+ });
17
+
18
+ async function tempDir(): Promise<string> {
19
+ const dir = await mkdtemp(join(tmpdir(), "oke-drizzle-env-"));
20
+ dirs.push(dir);
21
+ return dir;
22
+ }
23
+
24
+ const dualConfig = {
25
+ drivers: {
26
+ store: {
27
+ sql: { local: "sqlite", docker: "postgres", prod: "postgres", test: "memory" },
28
+ },
29
+ },
30
+ } as unknown as OkeConfig;
31
+
32
+ describe("resolveDrizzleKitEnv", () => {
33
+ test("local env → sqlite dialect + sqlite url", async () => {
34
+ const dir = await tempDir();
35
+ const ctx = await resolveDrizzleKitEnv(dir, dualConfig, "local");
36
+ expect(ctx.dialect).toBe("sqlite");
37
+ expect(ctx.overlay.OKE_DRIZZLE_DIALECT).toBe("sqlite");
38
+ expect(ctx.overlay.OKE_SQLITE_URL).toContain(".oke/app.sqlite");
39
+ expect(ctx.overlay.DATABASE_URL).toBeUndefined();
40
+ });
41
+
42
+ test("docker env → postgresql dialect + DATABASE_URL from compose env", async () => {
43
+ const dir = await tempDir();
44
+ await mkdir(join(dir, "docker"), { recursive: true });
45
+ await writeFile(
46
+ join(dir, "docker", ".env.docker"),
47
+ "DATABASE_URL=postgres://u:p@127.0.0.1:5432/oke\n",
48
+ );
49
+ const ctx = await resolveDrizzleKitEnv(dir, dualConfig, "docker");
50
+ expect(ctx.dialect).toBe("postgresql");
51
+ expect(ctx.overlay.OKE_DRIZZLE_DIALECT).toBe("postgresql");
52
+ expect(ctx.overlay.DATABASE_URL).toBe("postgres://u:p@127.0.0.1:5432/oke");
53
+ expect(ctx.overlay.OKE_SQLITE_URL).toBeUndefined();
54
+ });
55
+
56
+ test("docker dialect is postgresql even when DATABASE_URL unset (no URL guessing)", async () => {
57
+ const dir = await tempDir();
58
+ const ctx = await resolveDrizzleKitEnv(dir, dualConfig, "docker");
59
+ expect(ctx.dialect).toBe("postgresql");
60
+ });
61
+
62
+ test("unsupported driver id errors clearly", async () => {
63
+ const dir = await tempDir();
64
+ const bad = { drivers: { store: { sql: { local: "memory" } } } } as unknown as OkeConfig;
65
+ await expect(resolveDrizzleKitEnv(dir, bad, "local")).rejects.toThrow(/not supported/);
66
+ });
67
+ });
@@ -0,0 +1,78 @@
1
+ /**
2
+ * drizzle-kit child-env overlay — resolved from the configured `store.sql`
3
+ * driver for the active env (never guessed from `DATABASE_URL` presence).
4
+ */
5
+
6
+ import { resolveDriverId, type ConfigEnv, type OkeConfig } from "../config/index.ts";
7
+ import {
8
+ drizzleDialectFromSqlDriver,
9
+ type OkeDrizzleKitDialect,
10
+ } from "../drivers/drizzle-dialect.ts";
11
+ import { parseDotenv } from "../drivers/vault-dotenv-parse.ts";
12
+ import { resolveComposeEnvPath } from "../elements/vault/chain.ts";
13
+
14
+ /** Resolved drizzle env context for one project env. */
15
+ export interface DrizzleKitEnvContext {
16
+ /** Active env used for driver resolution. */
17
+ readonly env: ConfigEnv;
18
+ /** drizzle-kit dialect (from `SQL_DRIVER_TO_DRIZZLE_DIALECT`). */
19
+ readonly dialect: OkeDrizzleKitDialect;
20
+ /** Env overlay for the drizzle-kit child process. */
21
+ readonly overlay: Record<string, string>;
22
+ }
23
+
24
+ /**
25
+ * Load `docker/.env.docker` (or legacy `.env.docker`) as a name→value map.
26
+ *
27
+ * @param cwd - Project root
28
+ */
29
+ async function readComposeEnv(cwd: string): Promise<Map<string, string>> {
30
+ const { path } = resolveComposeEnvPath(cwd);
31
+ const file = Bun.file(path);
32
+ if (!(await file.exists())) return new Map();
33
+ try {
34
+ return parseDotenv(await file.text());
35
+ } catch {
36
+ return new Map();
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Resolve the drizzle-kit env context for a project + env.
42
+ *
43
+ * Dialect comes from the configured driver via the exhaustive
44
+ * `SQL_DRIVER_TO_DRIZZLE_DIALECT` map — a missing/unsupported driver id is a
45
+ * compile-time gap there, and a clear runtime error here (never a silent
46
+ * sqlite fallback).
47
+ *
48
+ * @param cwd - Project root
49
+ * @param config - Loaded `oke.config.ts` (null → sqlite/local defaults)
50
+ * @param env - Active env (`local` | `docker` for dev tooling)
51
+ */
52
+ export async function resolveDrizzleKitEnv(
53
+ cwd: string,
54
+ config: OkeConfig | null | undefined,
55
+ env: ConfigEnv,
56
+ ): Promise<DrizzleKitEnvContext> {
57
+ const driverId = resolveDriverId(config?.drivers?.store?.sql, env) ?? "sqlite";
58
+ if (driverId !== "sqlite" && driverId !== "postgres") {
59
+ throw new Error(
60
+ `oke db: store.sql driver "${driverId}" is not supported by drizzle-kit (sqlite | postgres)`,
61
+ );
62
+ }
63
+ const dialect = drizzleDialectFromSqlDriver(driverId);
64
+
65
+ const compose = env === "docker" ? await readComposeEnv(cwd) : new Map<string, string>();
66
+
67
+ const overlay: Record<string, string> = { OKE_DRIZZLE_DIALECT: dialect };
68
+ if (dialect === "postgresql") {
69
+ const url =
70
+ process.env.DATABASE_URL ?? compose.get("DATABASE_URL") ?? compose.get("OKE_STORE_SQL_URL");
71
+ if (url !== undefined) overlay.DATABASE_URL = url;
72
+ } else {
73
+ const url =
74
+ process.env.OKE_SQLITE_URL ?? compose.get("OKE_SQLITE_URL") ?? "file:.oke/app.sqlite";
75
+ overlay.OKE_SQLITE_URL = url;
76
+ }
77
+ return { env, dialect, overlay };
78
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Ensure a mode-aware `drizzle.config.ts` exists — created when absent,
3
+ * never overwritten when the user customised it.
4
+ */
5
+
6
+ import { resolve } from "node:path";
7
+
8
+ /** Mode-aware template (matches `packages/create-oke/template/drizzle.config.ts`). */
9
+ const DRIZZLE_CONFIG_TEMPLATE = `import { defineConfig } from "drizzle-kit";
10
+
11
+ /**
12
+ * Domain schema sync for \`oke db push|generate|migrate\`.
13
+ *
14
+ * Dialect comes from OKE's \`store.sql\` driver map (resolved by the CLI and
15
+ * injected as \`OKE_DRIZZLE_DIALECT\`) — never inferred from \`DATABASE_URL\`
16
+ * presence. \`src/schema.generated.ts\` is emitted from \`src/schema.decl.ts\`
17
+ * for the active dialect as a pre-step of \`oke db\` / \`oke dev\`.
18
+ *
19
+ * Versioned prod migrations land in \`./drizzle\` — never mix with \`.oke/\`.
20
+ */
21
+ const dialect = (process.env.OKE_DRIZZLE_DIALECT ?? "sqlite") as "sqlite" | "postgresql";
22
+
23
+ export default defineConfig({
24
+ dialect,
25
+ schema: "./src/schema.generated.ts",
26
+ out: "./drizzle",
27
+ dbCredentials: {
28
+ url:
29
+ dialect === "postgresql"
30
+ ? (process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL!)
31
+ : (process.env.OKE_SQLITE_URL ?? "file:.oke/app.sqlite"),
32
+ },
33
+ });
34
+ `;
35
+
36
+ /**
37
+ * Create `drizzle.config.ts` at the project root when it does not exist.
38
+ * Returns the absolute path and whether it was created.
39
+ *
40
+ * @param cwd - Project root
41
+ */
42
+ export async function ensureDrizzleConfig(
43
+ cwd: string,
44
+ ): Promise<{ readonly path: string; readonly created: boolean }> {
45
+ const path = resolve(cwd, "drizzle.config.ts");
46
+ const file = Bun.file(path);
47
+ if (await file.exists()) return { path, created: false };
48
+ await Bun.write(path, DRIZZLE_CONFIG_TEMPLATE);
49
+ return { path, created: true };
50
+ }
@@ -20,7 +20,7 @@ const SAMPLE: OkeConfig = {
20
20
  },
21
21
  signal: { local: "memory", docker: "postgres", prod: "postgres" },
22
22
  clock: { local: "memory", docker: "postgres", prod: "postgres" },
23
- vault: { local: "dotenv", docker: "dotenv", prod: "sops" },
23
+ vault: { local: "dotenv", docker: "openbao", prod: "openbao" },
24
24
  channel: {
25
25
  email: { local: "console", docker: "smtp", prod: "smtp" },
26
26
  },
@@ -10,6 +10,7 @@ import type { Manifest } from "../manifest/types.ts";
10
10
  const DEFAULT_SQL_IMAGE = "postgres:18-alpine";
11
11
  const DEFAULT_PGVECTOR_IMAGE = "pgvector/pgvector:pg17";
12
12
  const DEFAULT_KV_IMAGE = "redis:8-alpine";
13
+ const DEFAULT_VAULT_IMAGE = "openbao/openbao:2.6.1";
13
14
 
14
15
  /**
15
16
  * Extract protocol id from a driver ref.
@@ -66,6 +67,11 @@ export function defaultImagesFromConfig(config: OkeConfig): Readonly<Record<stri
66
67
  out["store.kv"] = DEFAULT_KV_IMAGE;
67
68
  }
68
69
 
70
+ const vault = dockerOrProdId(config.drivers?.vault);
71
+ if (vault === "openbao") {
72
+ out.vault = DEFAULT_VAULT_IMAGE;
73
+ }
74
+
69
75
  return out;
70
76
  }
71
77