okengine 0.10.2 → 0.10.3

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 (57) hide show
  1. package/package.json +1 -1
  2. package/site/content/docs/ai/mcp.mdx +0 -1
  3. package/site/content/docs/elements/ai.mdx +0 -6
  4. package/site/content/docs/elements/channel.mdx +0 -6
  5. package/site/content/docs/elements/clock.mdx +0 -6
  6. package/site/content/docs/elements/flow.mdx +0 -7
  7. package/site/content/docs/elements/gate.mdx +0 -6
  8. package/site/content/docs/elements/signal.mdx +0 -6
  9. package/site/content/docs/elements/store.mdx +8 -15
  10. package/site/content/docs/elements/vault.mdx +0 -6
  11. package/site/content/docs/get-started/basic-usage.mdx +0 -6
  12. package/site/content/docs/get-started/why.mdx +1 -2
  13. package/site/content/docs/index.mdx +0 -1
  14. package/site/content/docs/meta.json +0 -1
  15. package/site/content/docs/reference/fx.mdx +3 -3
  16. package/src/cli/ai-setup/ai-setup.test.ts +3 -1
  17. package/src/cli/build.ts +7 -8
  18. package/src/cli/dev.test.ts +88 -1
  19. package/src/cli/dev.ts +15 -13
  20. package/src/cli/doctor.ts +98 -5
  21. package/src/cli/load-config.images.test.ts +31 -1
  22. package/src/cli/load-config.ts +2 -2
  23. package/src/cli/safe-defaults.test.ts +1 -1
  24. package/src/compiler/extract.ts +12 -0
  25. package/src/compiler/fixtures/skyport/oke.config.ts +4 -2
  26. package/src/compiler/generate-adopt.ts +27 -1
  27. package/src/config/driver-defaults.test.ts +122 -0
  28. package/src/config/driver-defaults.ts +186 -0
  29. package/src/config/index.ts +97 -7
  30. package/src/docker/images-config.test.ts +168 -0
  31. package/src/kernel/boot-bind/channel.ts +12 -4
  32. package/src/kernel/boot-bind/clock.ts +3 -3
  33. package/src/kernel/boot-bind/gate.ts +7 -4
  34. package/src/kernel/boot-bind/journal.ts +3 -3
  35. package/src/kernel/boot-bind/signal.ts +3 -1
  36. package/src/kernel/boot-bind/store.test.ts +19 -0
  37. package/src/kernel/boot-bind/store.ts +24 -9
  38. package/src/kernel/boot-bind/vault.ts +3 -3
  39. package/site/content/docs/console/access.mdx +0 -29
  40. package/site/content/docs/console/ai.mdx +0 -35
  41. package/site/content/docs/console/architecture.mdx +0 -35
  42. package/site/content/docs/console/channels.mdx +0 -37
  43. package/site/content/docs/console/clock.mdx +0 -51
  44. package/site/content/docs/console/flows.mdx +0 -31
  45. package/site/content/docs/console/gates.mdx +0 -73
  46. package/site/content/docs/console/index.mdx +0 -54
  47. package/site/content/docs/console/manifest-diff.mdx +0 -34
  48. package/site/content/docs/console/meta.json +0 -24
  49. package/site/content/docs/console/overview.mdx +0 -40
  50. package/site/content/docs/console/plugins.mdx +0 -41
  51. package/site/content/docs/console/privacy.mdx +0 -32
  52. package/site/content/docs/console/runs.mdx +0 -50
  53. package/site/content/docs/console/signals.mdx +0 -40
  54. package/site/content/docs/console/store.mdx +0 -32
  55. package/site/content/docs/console/tenancy.mdx +0 -32
  56. package/site/content/docs/console/traces.mdx +0 -38
  57. package/site/content/docs/console/vault.mdx +0 -41
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "okengine",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
4
4
  "description": "One law. Eight elements. Ten exports. One package. One manifest. Every backend need is derived, never added.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -98,7 +98,6 @@ The docs content ships inside the `okengine` package, so the index your agent se
98
98
 
99
99
  ## Learn more
100
100
 
101
- - [Console · Access](/docs/console/access) — how operator tokens and grants work
102
101
  - [Agent contracts](/docs/ai/skills) — what agents are taught about the system they operate
103
102
  - [Flow](/docs/elements/flow) — the effects the MCP reads back
104
103
 
@@ -289,7 +289,6 @@ Bound it at declaration: `maxSteps` caps iterations, `budget.maxCostPerRun` caps
289
289
 
290
290
  - [Flow](/docs/elements/flow) — `fx.ask` and `fx.search` inside `do`
291
291
  - [Store](/docs/elements/store) — `store.index`, the home of embeddings
292
- - [Console · AI](/docs/console/ai) — prompts, versions, cost per run
293
292
 
294
293
  ## Next
295
294
 
@@ -304,9 +303,4 @@ Bound it at declaration: `maxSteps` caps iterations, `budget.maxCostPerRun` caps
304
303
  description="Eight elements overview."
305
304
  href="/docs/get-started/introduction"
306
305
  />
307
- <Card
308
- title="Console"
309
- description="Panels derived from the Manifest."
310
- href="/docs/console/overview"
311
- />
312
306
  </Cards>
@@ -326,7 +326,6 @@ Confirm a visible OS notification titled `okengine webpush interop`.
326
326
  ## Learn more
327
327
 
328
328
  - [Flow](/docs/elements/flow) — `fx.send` inside `do`
329
- - [Console · Channels](/docs/console/channels) — receipts, attempts, suppression
330
329
  - [Environment Variables](/docs/reference/environment-variables) — Channel boot credentials
331
330
  - [Configuration](/docs/reference/configuration) — `drivers.channel.*` maps
332
331
  - [i18n](/docs/reference/i18n) — ICU / `fx.t` vs Channel `{{field}}` catalogs
@@ -341,9 +340,4 @@ Confirm a visible OS notification titled `okengine webpush interop`.
341
340
  description="Eight elements overview."
342
341
  href="/docs/get-started/introduction"
343
342
  />
344
- <Card
345
- title="Console"
346
- description="Panels derived from the Manifest."
347
- href="/docs/console/overview"
348
- />
349
343
  </Cards>
@@ -195,7 +195,6 @@ They are not sharing one CronStore. In-memory leases are per process. Point ever
195
195
 
196
196
  - [Flow](/docs/elements/flow) — `on(trigger, flow)` and the `fx` surface
197
197
  - [fx · Runs](/docs/reference/fx#runs-observability-read) — native SLO checkers via `on(every(…))` + `fx.runs`
198
- - [Console · Clock](/docs/console/clock) — health numbers, pause / edit / wake early
199
198
  - [Signal](/docs/elements/signal) — reacting to events instead of time
200
199
 
201
200
  ## Next
@@ -207,9 +206,4 @@ They are not sharing one CronStore. In-memory leases are per process. Point ever
207
206
  description="Eight elements overview."
208
207
  href="/docs/get-started/introduction"
209
208
  />
210
- <Card
211
- title="Console"
212
- description="Panels derived from the Manifest."
213
- href="/docs/console/overview"
214
- />
215
209
  </Cards>
@@ -320,8 +320,6 @@ Only that run fails — the schedule keeps firing and the process does not exit.
320
320
  - [fx](/docs/reference/fx) — full `fx` surface, including ICU `fx.t` / `fx.locale`
321
321
  - [Signal](/docs/elements/signal) — delivery physics (`once` · `broadcast` · `live`)
322
322
  - [Clock](/docs/elements/clock) — schedules and durable sleep
323
- - [Console · Flows](/docs/console/flows) — the Manifest-derived panel
324
- - [Runs](/docs/console/runs) — how a flow execution is observed
325
323
 
326
324
  ## Next
327
325
 
@@ -332,9 +330,4 @@ Only that run fails — the schedule keeps firing and the process does not exit.
332
330
  description="Eight elements overview."
333
331
  href="/docs/get-started/introduction"
334
332
  />
335
- <Card
336
- title="Console"
337
- description="Panels derived from the Manifest."
338
- href="/docs/console/overview"
339
- />
340
333
  </Cards>
@@ -308,7 +308,6 @@ Keying an authenticated endpoint by IP punishes shared NAT.
308
308
  - [fx](/docs/reference/fx) — `fx.auth`, `fx.operator`, `fx.principal`
309
309
  - [Client](/docs/reference/client) — Bearer `createClient` + `okengine/client/auth`
310
310
  - [Plugins](/docs/plugins) — username, magic link, OTP, TOTP, passkeys
311
- - [Console · Gates](/docs/console/gates) — decision audit and posture
312
311
  - [Vault](/docs/elements/vault) — credentials your policies protect
313
312
 
314
313
  ## Next
@@ -316,9 +315,4 @@ Keying an authenticated endpoint by IP punishes shared NAT.
316
315
  <Cards>
317
316
  <Card title="Vault" description="Continue to Vault." href="/docs/elements/vault" />
318
317
  <Card title="Plugins" description="Sign-in method plugins under /auth." href="/docs/plugins" />
319
- <Card
320
- title="Console"
321
- description="Panels derived from the Manifest."
322
- href="/docs/console/overview"
323
- />
324
318
  </Cards>
@@ -291,7 +291,6 @@ At-least-once: crash-after-claim reclaims when the lease expires, and a handler
291
291
  ## Learn more
292
292
 
293
293
  - [Flow](/docs/elements/flow) — `on(trigger, flow)` and `fx.emit`
294
- - [Console · Signals](/docs/console/signals) — topology, delivery stats, DLQ replay
295
294
  - [Clock](/docs/elements/clock) — scheduled and delayed work
296
295
 
297
296
  ## Next
@@ -303,9 +302,4 @@ At-least-once: crash-after-claim reclaims when the lease expires, and a handler
303
302
  description="Eight elements overview."
304
303
  href="/docs/get-started/introduction"
305
304
  />
306
- <Card
307
- title="Console"
308
- description="Panels derived from the Manifest."
309
- href="/docs/console/overview"
310
- />
311
305
  </Cards>
@@ -490,9 +490,8 @@ await fx.store(sessions).set(`sess:${token}`, { userId }, "7d");
490
490
  const keys = await fx.store(sessions).list("sess:");
491
491
  ```
492
492
 
493
- **Consequence:** `list` is how the [Console · Store](/docs/console/store) browser enumerates a
494
- namespace. Prefer stable key prefixes (`user:`, `sess:`) so both your Flows and the Console stay
495
- readable.
493
+ **Consequence:** `list` enumerates a namespace. Prefer stable key prefixes (`user:`, `sess:`) so
494
+ your Flows and operator tools stay readable.
496
495
 
497
496
  ### Per-environment KV drivers
498
497
 
@@ -637,9 +636,9 @@ images: {
637
636
 
638
637
  **Known limit:** `fs` is single-host. Under horizontal scale each replica sees its own filesystem (silently inconsistent object views). Boot prints a one-shot warning when `drivers.store.files` is `fs`. Use `s3` for docker/prod (create-oke templates already do).
639
638
 
640
- <Callout title="Console can browse, not edit bytes">
641
- The [Console · Store](/docs/console/store) lists keys and can delete them. Direct edit is **KV +
642
- SQL only** — blob bodies are not patched from the Console.
639
+ <Callout title="Operator tools can browse, not edit bytes">
640
+ Operator tools can list keys and delete them. Direct edit is **KV + SQL only** — blob bodies are
641
+ not patched.
643
642
  </Callout>
644
643
 
645
644
  ### Images — `image` / `putImage`
@@ -861,9 +860,9 @@ Container images come from the `images` map — change the vendor by changing th
861
860
 
862
861
  Columns tagged `.pii()` or `.sensitive()` are masked at the store boundary — flows, logs, and the Console see a mask, not the value. Revealing cleartext PII requires an explicit `pii:reveal` gate on the flow, so access is a permission, not a convention.
863
862
 
864
- Optional `description` on a store (and `.describe("…")` on fields) becomes the human title in the
865
- [Console · Store](/docs/console/store) browser. Single-table `reads` / `writes` in the Manifest
866
- power cache invalidation and that same PII masking — see the Callout under [Querying by hand](#querying-by-hand).
863
+ Optional `description` on a store (and `.describe("…")` on fields) becomes its human title in
864
+ operator tools. Single-table `reads` / `writes` in the Manifest power cache invalidation and that
865
+ same PII masking — see the Callout under [Querying by hand](#querying-by-hand).
867
866
 
868
867
  ## Troubleshooting
869
868
 
@@ -915,7 +914,6 @@ Unlike KV/Files, unset `drivers.store.index` does **not** promote to `pgvector`
915
914
  - [Flow](/docs/elements/flow) — the `fx.store` session inside `do`
916
915
  - [AI](/docs/elements/ai) — `ai.embed` into a vector `store.index`, searched via `fx.search`
917
916
  - [Gate](/docs/elements/gate) — `pii:reveal` and other permissions on flows
918
- - [Console · Store](/docs/console/store) — browse data, cache keys, PII masking
919
917
  - [CLI Reference](/docs/reference/cli) — `oke db push` · `generate` · `migrate`
920
918
  - [Configuration](/docs/reference/configuration) — `drivers.store` maps and `images` pins
921
919
  - [Environment variables](/docs/reference/environment-variables) — Redis · S3 · meilisearch URLs
@@ -929,9 +927,4 @@ Unlike KV/Files, unset `drivers.store.index` does **not** promote to `pgvector`
929
927
  description="Eight elements overview."
930
928
  href="/docs/get-started/introduction"
931
929
  />
932
- <Card
933
- title="Console"
934
- description="Panels derived from the Manifest."
935
- href="/docs/console/overview"
936
- />
937
930
  </Cards>
@@ -251,7 +251,6 @@ Set the new value (`oke vault set` or the Console). The Console's rotation view
251
251
 
252
252
  ## Learn more
253
253
 
254
- - [Console · Vault](/docs/console/vault) — fingerprints, resolution chain, rotation blast radius
255
254
  - [Flow](/docs/elements/flow) — how `fx.vault` reads secrets inside `do`
256
255
  - [CLI Reference](/docs/reference/cli) — `oke vault set` · `list` · `import`
257
256
 
@@ -264,9 +263,4 @@ Set the new value (`oke vault set` or the Console). The Console's rotation view
264
263
  description="Eight elements overview."
265
264
  href="/docs/get-started/introduction"
266
265
  />
267
- <Card
268
- title="Console"
269
- description="Panels derived from the Manifest."
270
- href="/docs/console/overview"
271
- />
272
266
  </Cards>
@@ -146,7 +146,6 @@ bun test
146
146
  - [Flow](/docs/elements/flow) — triggers, contracts, effects, and composition
147
147
  - [Store](/docs/elements/store) — SQL, KV, files, index
148
148
  - [fx](/docs/reference/fx) — the full effect surface
149
- - [Console · Overview](/docs/console/overview) — Manifest-derived panels
150
149
 
151
150
  ## Next
152
151
 
@@ -161,9 +160,4 @@ bun test
161
160
  description="SQL, KV, files, index, and declarative resources."
162
161
  href="/docs/elements/store"
163
162
  />
164
- <Card
165
- title="Console"
166
- description="Manifest-derived flows, effects, traces, and architecture."
167
- href="/docs/console/overview"
168
- />
169
163
  </Cards>
@@ -49,7 +49,7 @@ shared lifecycle, optional live DB config, nothing to re-copy.
49
49
  Your observability stack learned your routes from sampled traffic. The Flow
50
50
  you deployed an hour ago is invisible until someone wires it by hand.
51
51
 
52
- **OKE derives:** the [Console](/docs/console/overview) reads the Manifest —
52
+ **OKE derives:** its operator interface reads the Manifest —
53
53
  flows, effects, traces, architecture — current on every save, in dev and prod
54
54
  (`:6533`).
55
55
 
@@ -118,7 +118,6 @@ ninth element — the set of eight is closed.
118
118
  - [Flow](/docs/elements/flow) — how effects are recorded and inferred
119
119
  - [Vault](/docs/elements/vault) — fail-loud secret contracts
120
120
  - [Plugins](/docs/reference/plugins) — the official HTTP glue set
121
- - [Console · Overview](/docs/console/overview) — the Manifest-derived operator UI
122
121
 
123
122
  ## Next
124
123
 
@@ -31,7 +31,6 @@ on(orderPlaced, sendReceipt);
31
31
  description="Official extensions — security, ops, performance."
32
32
  href="/docs/plugins"
33
33
  />
34
- <Card title="Console" description="Manifest-derived operator panels." href="/docs/console" />
35
34
  <Card
36
35
  title="Deployment"
37
36
  description="Compose, Swarm, Kubernetes, reverse proxy."
@@ -6,7 +6,6 @@
6
6
  "get-started",
7
7
  "elements",
8
8
  "plugins",
9
- "console",
10
9
  "deployment",
11
10
  "recipes",
12
11
  "providers",
@@ -259,9 +259,9 @@ must log who started the call chain.
259
259
  ## Not on `fx`
260
260
 
261
261
  <Callout title="No fx.metric">
262
- Investigated and declined. [Runs](/docs/console/runs) already own per-invocation observability as
263
- wide events. Native alerting is `fx.runs` + Clock + Channel — not a second counter/gauge API.
264
- Optional OTLP export for existing Grafana/Datadog stacks is additive and never required.
262
+ Investigated and declined. `fx.runs` already provides per-invocation observability as wide events.
263
+ Native alerting is `fx.runs` + Clock + Channel — not a second counter/gauge API. Optional OTLP
264
+ export for existing Grafana/Datadog stacks is additive and never required.
265
265
  </Callout>
266
266
 
267
267
  ## Learn more
@@ -117,7 +117,9 @@ export default defineConfig({
117
117
  },
118
118
  },
119
119
  images: {
120
- "store.sql": "postgres:18-alpine",
120
+ store: {
121
+ sql: "postgres:18-alpine",
122
+ },
121
123
  },
122
124
  });
123
125
  `,
package/src/cli/build.ts CHANGED
@@ -2,9 +2,6 @@
2
2
  * `oke build --target edge` — tree-shaken kernel profile.
3
3
  */
4
4
 
5
- import { writeFile } from "node:fs/promises";
6
- import { join } from "node:path";
7
-
8
5
  /** Options for {@link runBuild}. */
9
6
  export interface BuildOptions {
10
7
  readonly target?: "bun" | "node" | "edge";
@@ -21,8 +18,9 @@ export interface BuildOptions {
21
18
  }) => Promise<{ success: boolean; logs: string }>;
22
19
  /**
23
20
  * Inject `.adopt()` barrel regeneration (tests). Default: real
24
- * `generateAdoptBarrel` + write to `<rootDir>/src/flows/generated.ts`.
25
- * Returns the unit names written, for the status line.
21
+ * `generateAdoptBarrel` + atomic write to `<rootDir>/src/flows/generated.ts`
22
+ * (`generated.ts.tmp` → rename). Returns the unit names written, for the
23
+ * status line.
26
24
  */
27
25
  readonly syncAdoptBarrel?: (rootDir: string) => Promise<readonly string[]>;
28
26
  }
@@ -33,14 +31,15 @@ export interface BuildOptions {
33
31
  * pre-step). A real file on disk, not a virtual module: identical resolution
34
32
  * under `oke dev`'s runtime `import()` and `oke build`'s `Bun.build()`
35
33
  * (investigated — a virtual-module Bun plugin does not resolve consistently
36
- * across those two paths).
34
+ * across those two paths). Written atomically so a concurrent reader never
35
+ * sees a torn file.
37
36
  *
38
37
  * @param rootDir - Project root
39
38
  */
40
39
  async function defaultSyncAdoptBarrel(rootDir: string): Promise<readonly string[]> {
41
- const { generateAdoptBarrel } = await import("../compiler/generate-adopt.ts");
40
+ const { generateAdoptBarrel, writeAdoptBarrel } = await import("../compiler/generate-adopt.ts");
42
41
  const { source, units } = await generateAdoptBarrel({ rootDir });
43
- await writeFile(join(rootDir, "src/flows/generated.ts"), source);
42
+ await writeAdoptBarrel(rootDir, source);
44
43
  return units;
45
44
  }
46
45
 
@@ -7,9 +7,14 @@
7
7
  */
8
8
 
9
9
  import { afterEach, describe, expect, test } from "bun:test";
10
- import { mkdtemp } from "node:fs/promises";
10
+ import { mkdir, mkdtemp, readdir } from "node:fs/promises";
11
11
  import { tmpdir } from "node:os";
12
12
  import { join, resolve } from "node:path";
13
+ import {
14
+ ADOPT_BARREL_FILE,
15
+ generateAdoptBarrel,
16
+ writeAdoptBarrel,
17
+ } from "../compiler/generate-adopt.ts";
13
18
  import type { Manifest } from "../manifest/types.ts";
14
19
  import type { WideEvent } from "../runs/types.ts";
15
20
  import { isDataEnvelope, MCP_DATA_KIND } from "../mcp/data.ts";
@@ -544,6 +549,88 @@ describe("oke dev hot reload", () => {
544
549
  }, 60_000);
545
550
  });
546
551
 
552
+ describe("oke dev syncAdoptBarrel atomic write", () => {
553
+ test("default path regenerates generated.ts and leaves no .tmp behind", async () => {
554
+ const dir = await mkdtemp(join(tmpdir(), "oke-dev-adopt-atomic-"));
555
+ await mkdir(join(dir, "src/flows/notes"), { recursive: true });
556
+ await Bun.write(join(dir, "src/flows/notes/index.ts"), "export {};\n");
557
+ await Bun.write(join(dir, "src/app.ts"), "export {};\n");
558
+ await Bun.write(join(dir, "src/flows", ADOPT_BARREL_FILE), "// stale stub\n");
559
+
560
+ const { code } = await runDev({
561
+ cwd: dir,
562
+ dryRun: true,
563
+ stdinIsTTY: false,
564
+ write: () => {},
565
+ });
566
+ expect(code).toBe(0);
567
+
568
+ const barrelPath = join(dir, "src/flows", ADOPT_BARREL_FILE);
569
+ const text = await Bun.file(barrelPath).text();
570
+ expect(text).toContain('export * as notes from "./notes/index.ts";');
571
+ expect(text).not.toContain("stale stub");
572
+ expect(await Bun.file(`${barrelPath}.tmp`).exists()).toBe(false);
573
+ const leftovers = (await readdir(join(dir, "src/flows"))).filter((f) => f.includes(".tmp"));
574
+ expect(leftovers).toEqual([]);
575
+ });
576
+
577
+ test("writeAdoptBarrel uses temp then rename — concurrent readers never see a torn file", async () => {
578
+ const dir = await mkdtemp(join(tmpdir(), "oke-dev-adopt-race-"));
579
+ const flowsDir = join(dir, "src/flows");
580
+ await mkdir(flowsDir, { recursive: true });
581
+ const target = join(flowsDir, ADOPT_BARREL_FILE);
582
+
583
+ // Two complete, same-length payloads so a mid-write truncate would yield
584
+ // mixed or short content that fails both equality checks.
585
+ const markerA = "A";
586
+ const markerB = "B";
587
+ const body = (marker: string) => `// ${marker}\n` + `${marker.repeat(64)}\n`.repeat(2_000);
588
+ const payloadA = body(markerA);
589
+ const payloadB = body(markerB);
590
+ expect(payloadA.length).toBe(payloadB.length);
591
+ expect(payloadA).not.toBe(payloadB);
592
+
593
+ await writeAdoptBarrel(dir, payloadA);
594
+ expect(await Bun.file(target).text()).toBe(payloadA);
595
+
596
+ const reads: string[] = [];
597
+ let stop = false;
598
+ const reader = (async () => {
599
+ while (!stop) {
600
+ try {
601
+ reads.push(await Bun.file(target).text());
602
+ } catch {
603
+ // Target briefly missing only if rename races a delete — ignore.
604
+ }
605
+ // Yield so writers interleave; Bun.sleep(0) is enough on this loop.
606
+ await Bun.sleep(0);
607
+ }
608
+ })();
609
+
610
+ for (let i = 0; i < 80; i++) {
611
+ await writeAdoptBarrel(dir, i % 2 === 0 ? payloadB : payloadA);
612
+ }
613
+ stop = true;
614
+ await reader;
615
+
616
+ expect(reads.length).toBeGreaterThan(10);
617
+ for (const sample of reads) {
618
+ // Every observation is a complete prior or complete next payload —
619
+ // never a truncated prefix, never a mix of A/B markers.
620
+ const ok = sample === payloadA || sample === payloadB;
621
+ expect(ok).toBe(true);
622
+ expect(sample.length).toBe(payloadA.length);
623
+ expect(sample.includes(markerA) && sample.includes(markerB)).toBe(false);
624
+ }
625
+ expect(await Bun.file(`${target}.tmp`).exists()).toBe(false);
626
+
627
+ // Same helper the default `oke dev` path uses end-to-end.
628
+ const { source } = await generateAdoptBarrel({ rootDir: dir });
629
+ await writeAdoptBarrel(dir, source);
630
+ expect(await Bun.file(target).text()).toBe(source);
631
+ });
632
+ });
633
+
547
634
  describe("oke dev mode resolution", () => {
548
635
  test("non-TTY + unset mode → local, zero ask, zero docker, no save", async () => {
549
636
  const dir = await mkdtemp(join(tmpdir(), "oke-dev-mode-nontty-"));
package/src/cli/dev.ts CHANGED
@@ -178,11 +178,13 @@ export interface DevOptions {
178
178
  readonly onDbAutoPush?: (filename: string) => void;
179
179
  /**
180
180
  * Injectable `.adopt()` barrel regeneration (tests). Default: real
181
- * `generateAdoptBarrel` + write to `<cwd>/src/flows/generated.ts`. Runs
182
- * once per `oke dev` session, before the entry is resolved/imported —
183
- * a real file on disk, so `oke dev`'s runtime `import()` and `oke build`'s
184
- * `Bun.build()` resolve it identically (unlike a virtual-module Bun
185
- * plugin, which does not — see `compiler/generate-adopt.ts`).
181
+ * `generateAdoptBarrel` + atomic write to `<cwd>/src/flows/generated.ts`
182
+ * (`generated.ts.tmp` → rename). Runs once per `oke dev` session, before
183
+ * the entry is resolved/imported — a real file on disk, so `oke dev`'s
184
+ * runtime `import()` and `oke build`'s `Bun.build()` resolve it
185
+ * identically (unlike a virtual-module Bun plugin, which does not — see
186
+ * `compiler/generate-adopt.ts`). Atomic so a concurrent import during
187
+ * `bun --hot` never sees a torn mid-write file.
186
188
  *
187
189
  * @param cwd - Project root
188
190
  */
@@ -346,18 +348,18 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
346
348
  const cwd = options.cwd ?? process.cwd();
347
349
 
348
350
  // One-shot `.adopt()` barrel regen for the session — real file on disk
349
- // (`src/flows/generated.ts`), written before the entry is resolved/imported
350
- // below so a freshly-added flows unit is adoptable without a hand edit.
351
- // Best-effort like `syncDevSchema` below: a project with no `src/flows`
352
- // yet (or a synthetic test tree) never blocks the dev session.
351
+ // (`src/flows/generated.ts`), written atomically (tmp rename) before the
352
+ // entry is resolved/imported below so a freshly-added flows unit is
353
+ // adoptable without a hand edit. Best-effort like `syncDevSchema` below:
354
+ // a project with no `src/flows` yet (or a synthetic test tree) never
355
+ // blocks the dev session.
353
356
  const syncAdoptBarrel =
354
357
  options.syncAdoptBarrel ??
355
358
  (async (root: string) => {
356
- const { generateAdoptBarrel } = await import("../compiler/generate-adopt.ts");
357
- const { mkdir, writeFile } = await import("node:fs/promises");
359
+ const { generateAdoptBarrel, writeAdoptBarrel } =
360
+ await import("../compiler/generate-adopt.ts");
358
361
  const { source, units } = await generateAdoptBarrel({ rootDir: root });
359
- await mkdir(resolve(root, "src/flows"), { recursive: true });
360
- await writeFile(resolve(root, "src/flows/generated.ts"), source);
362
+ await writeAdoptBarrel(root, source);
361
363
  return units;
362
364
  });
363
365
  try {
package/src/cli/doctor.ts CHANGED
@@ -3,17 +3,70 @@
3
3
  */
4
4
 
5
5
  import { resolve } from "node:path";
6
+ import type { ConfigEnv, DriverRef, DriversConfig, EnvDriverMap } from "../config/index.ts";
7
+ import { resolveEffectiveDrivers, type EffectiveDriversConfig } from "../config/driver-defaults.ts";
6
8
  import type { Manifest } from "../manifest/types.ts";
7
9
  import { APP_PORT, CONSOLE_PORT, MCP_PORT } from "../runtime/types.ts";
8
10
  import { hasFlag, wantsJson } from "./args.ts";
9
11
  import { checkManifestPiiAsks } from "./doctor-pii.ts";
10
12
  import { EXIT_OK, EXIT_RUNTIME } from "./exit.ts";
11
- import { loadManifest } from "./load-config.ts";
13
+ import { loadManifest, loadOkeConfig } from "./load-config.ts";
12
14
  import { isPortInUse } from "./ports.ts";
13
15
  import { schemaFingerprint, readSchemaFingerprint } from "./schema.ts";
14
16
 
15
17
  export { isPortInUse } from "./ports.ts";
16
18
 
19
+ /**
20
+ * `label → merged EnvDriverMap` pairs, in the fixed order shown by
21
+ * {@link formatDriversSummary} / the `drivers` JSON key.
22
+ *
23
+ * @param drivers - Fully-resolved drivers config
24
+ */
25
+ function flattenEffectiveDrivers(
26
+ drivers: EffectiveDriversConfig,
27
+ ): ReadonlyArray<readonly [string, EnvDriverMap]> {
28
+ return [
29
+ ["store.sql", drivers.store.sql],
30
+ ["store.kv", drivers.store.kv],
31
+ ["store.files", drivers.store.files],
32
+ ["signal", drivers.signal],
33
+ ["clock", drivers.clock],
34
+ ["journal", drivers.journal],
35
+ ["vault", drivers.vault],
36
+ ["channel.email", drivers.channel.email],
37
+ ["channel.sms", drivers.channel.sms],
38
+ ];
39
+ }
40
+
41
+ /**
42
+ * @param ref - Driver ref (string id, rich `{ driver }` object, or unset)
43
+ */
44
+ function driverRefId(ref: DriverRef | undefined): string {
45
+ if (ref === undefined) return "—";
46
+ return typeof ref === "string" ? ref : ref.driver;
47
+ }
48
+
49
+ /**
50
+ * Render the fully-resolved drivers config as aligned text lines — every
51
+ * default plus every override, per env, for a developer whose own
52
+ * `oke.config.ts` only pins a handful of keys.
53
+ *
54
+ * @param drivers - Fully-resolved drivers config
55
+ * @param configEnv - Active env (marked in the active-driver column)
56
+ */
57
+ function formatDriversSummary(drivers: EffectiveDriversConfig, configEnv: ConfigEnv): string {
58
+ const rows = flattenEffectiveDrivers(drivers);
59
+ const nameWidth = Math.max(...rows.map(([name]) => name.length));
60
+ const activeWidth = Math.max(...rows.map(([, map]) => driverRefId(map[configEnv]).length));
61
+ const lines = [`drivers (env=${configEnv}):`];
62
+ for (const [name, map] of rows) {
63
+ const active = driverRefId(map[configEnv]);
64
+ const full = `local=${driverRefId(map.local)} docker=${driverRefId(map.docker)} test=${driverRefId(map.test)} prod=${driverRefId(map.prod)}`;
65
+ lines.push(` ${name.padEnd(nameWidth)} ${active.padEnd(activeWidth)} { ${full} }`);
66
+ }
67
+ return lines.join("\n");
68
+ }
69
+
17
70
  /** One doctor finding. */
18
71
  export interface DoctorFinding {
19
72
  readonly code:
@@ -55,6 +108,19 @@ export interface DoctorOptions {
55
108
  }>;
56
109
  /** Skip live DB drift probe (default false). */
57
110
  readonly skipDbDrift?: boolean;
111
+ /**
112
+ * Inject the `drivers` block directly (tests) — skips loading
113
+ * `oke.config.ts` from disk. When omitted, `oke doctor` best-effort loads
114
+ * `oke.config.ts` from {@link cwd}; a project without one just gets the
115
+ * real, untouched defaults in the drivers summary.
116
+ */
117
+ readonly driversConfig?: DriversConfig;
118
+ /**
119
+ * Active {@link ConfigEnv} for the drivers summary. Default: `docker` when
120
+ * `OKE_DOCKER=1` (via {@link env}), else `local` — the same resolution
121
+ * `oke boot` uses without an explicit `env`.
122
+ */
123
+ readonly configEnv?: ConfigEnv;
58
124
  readonly write?: (text: string) => void;
59
125
  /** Write hints / progress (defaults to stderr). */
60
126
  readonly writeErr?: (text: string) => void;
@@ -92,6 +158,20 @@ export async function runDoctor(options: DoctorOptions = {}): Promise<{
92
158
  const env = options.env ?? ((k) => Bun.env[k] ?? process.env[k]);
93
159
  const secretNames = options.secrets ?? (manifest?.vault ? Object.keys(manifest.vault) : []);
94
160
 
161
+ // Fully-resolved `drivers.*` — real defaults + every override merged, so
162
+ // the picture is complete even when `oke.config.ts` only pins one key.
163
+ let driversConfig = options.driversConfig;
164
+ if (driversConfig === undefined) {
165
+ try {
166
+ driversConfig = (await loadOkeConfig(cwd)).config.drivers;
167
+ } catch {
168
+ // No oke.config.ts (or it failed to load) — show real defaults only.
169
+ }
170
+ }
171
+ const configEnv: ConfigEnv =
172
+ options.configEnv ?? (env("OKE_DOCKER") === "1" ? "docker" : "local");
173
+ const effectiveDrivers = resolveEffectiveDrivers(driversConfig);
174
+
95
175
  for (const name of secretNames) {
96
176
  const value = env(name);
97
177
  if (value === undefined || value.length === 0) {
@@ -177,7 +257,9 @@ export async function runDoctor(options: DoctorOptions = {}): Promise<{
177
257
  const code = ok ? EXIT_OK : EXIT_RUNTIME;
178
258
 
179
259
  if (json) {
180
- write(`${JSON.stringify({ ok, findings }, null, 2)}\n`);
260
+ write(
261
+ `${JSON.stringify({ ok, findings, drivers: { env: configEnv, resolved: effectiveDrivers } }, null, 2)}\n`,
262
+ );
181
263
  if (!ok) {
182
264
  writeErr("Hint: fix error-severity findings, then re-run oke doctor.\n");
183
265
  }
@@ -186,6 +268,7 @@ export async function runDoctor(options: DoctorOptions = {}): Promise<{
186
268
 
187
269
  if (findings.length === 0) {
188
270
  write("oke doctor: ok\n");
271
+ write(`${formatDriversSummary(effectiveDrivers, configEnv)}\n`);
189
272
  return { code: EXIT_OK, findings };
190
273
  }
191
274
 
@@ -193,6 +276,7 @@ export async function runDoctor(options: DoctorOptions = {}): Promise<{
193
276
  for (const f of findings) {
194
277
  write(` [${f.severity}] ${f.code}: ${f.message}\n`);
195
278
  }
279
+ write(`${formatDriversSummary(effectiveDrivers, configEnv)}\n`);
196
280
  return { code, findings };
197
281
  }
198
282
 
@@ -208,16 +292,25 @@ export async function doctorCli(args: readonly string[]): Promise<number> {
208
292
  }
209
293
 
210
294
  let manifestPath: string | undefined;
295
+ let configEnv: ConfigEnv | undefined;
211
296
  const json = wantsJson(args);
212
297
  for (let i = 0; i < args.length; i++) {
213
298
  const a = args[i]!;
214
299
  if (a === "--manifest" || a === "-m") manifestPath = args[++i];
215
- else if (a === "--help" || a === "-h") {
216
- console.log(`oke doctor [--manifest|-m path] [--json|-j]
300
+ else if (a === "--env" || a === "-e") {
301
+ const value = args[++i];
302
+ if (value === "local" || value === "docker" || value === "test" || value === "prod") {
303
+ configEnv = value;
304
+ }
305
+ } else if (a === "--help" || a === "-h") {
306
+ console.log(`oke doctor [--manifest|-m path] [--env|-e local|docker|test|prod] [--json|-j]
217
307
  oke doctor --diff|-d [--before|-b <path> --after|-a <path>] [--base|-B <branch>]
218
308
 
219
309
  Verify secrets, ports, schema drift, and PII→model egress before serving.
310
+ Also prints drivers.* fully resolved (every default + every override merged)
311
+ for the active env, from oke.config.ts.
220
312
 
313
+ --env Env to resolve drivers for (default: docker when OKE_DOCKER=1, else local).
221
314
  --diff CI gate: block undeclared contract breaks (Manifest Diff).
222
315
  Default baseline is git merge-base (main/master) vs the working
223
316
  tree; pass --before/--after for an explicit comparison.
@@ -227,6 +320,6 @@ Verify secrets, ports, schema drift, and PII→model egress before serving.
227
320
  return EXIT_OK;
228
321
  }
229
322
  }
230
- const { code } = await runDoctor({ manifestPath, json });
323
+ const { code } = await runDoctor({ manifestPath, configEnv, json });
231
324
  return code;
232
325
  }