okengine 0.19.9 → 0.21.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 (156) hide show
  1. package/AGENTS.md +1 -1
  2. package/package.json +2 -1
  3. package/site/content/docs/client/auth.mdx +1 -2
  4. package/site/content/docs/client/calling.mdx +125 -37
  5. package/site/content/docs/client/index.mdx +7 -7
  6. package/site/content/docs/client/react.mdx +5 -0
  7. package/site/content/docs/elements/channel/email.mdx +25 -36
  8. package/site/content/docs/elements/channel/index.mdx +88 -46
  9. package/site/content/docs/elements/channel/push.mdx +7 -9
  10. package/site/content/docs/elements/channel/sms.mdx +6 -4
  11. package/site/content/docs/elements/channel/whatsapp.mdx +11 -9
  12. package/site/content/docs/elements/clock/index.mdx +14 -25
  13. package/site/content/docs/elements/flow/http.mdx +42 -23
  14. package/site/content/docs/elements/flow/index.mdx +41 -30
  15. package/site/content/docs/elements/flow/routing.mdx +166 -122
  16. package/site/content/docs/elements/gate/rls.mdx +2 -2
  17. package/site/content/docs/elements/gate/tenancy.mdx +1 -1
  18. package/site/content/docs/elements/store/files.mdx +11 -5
  19. package/site/content/docs/elements/store/index.mdx +13 -6
  20. package/site/content/docs/elements/store/kv.mdx +8 -2
  21. package/site/content/docs/elements/store/search.mdx +5 -5
  22. package/site/content/docs/elements/store/sql.mdx +100 -39
  23. package/site/content/docs/elements/vault/index.mdx +14 -13
  24. package/site/content/docs/elements/vault/secrets.mdx +6 -3
  25. package/site/content/docs/index.mdx +1 -1
  26. package/site/content/docs/plugins/magic-link.mdx +4 -3
  27. package/site/content/docs/plugins/otp.mdx +4 -3
  28. package/site/content/docs/plugins/two-factor.mdx +4 -0
  29. package/site/content/docs/providers/index.mdx +1 -1
  30. package/site/content/docs/recipes/index.mdx +1 -1
  31. package/site/content/docs/recipes/rustfs.mdx +1 -1
  32. package/site/content/docs/reference/cli.mdx +7 -4
  33. package/site/content/docs/reference/configuration.mdx +8 -8
  34. package/site/content/docs/reference/errors.mdx +229 -55
  35. package/site/content/docs/reference/fx.mdx +22 -9
  36. package/site/content/docs/reference/i18n.mdx +4 -4
  37. package/site/content/docs/reference/plugins.mdx +4 -4
  38. package/site/content/docs/understand/the-architecture.mdx +2 -2
  39. package/site/content/docs/understand/try-it.mdx +759 -24
  40. package/src/cli/ai-setup/ai-setup.test.ts +40 -0
  41. package/src/cli/ai-setup/apply.ts +28 -46
  42. package/src/cli/build.test.ts +3 -3
  43. package/src/cli/build.ts +5 -5
  44. package/src/cli/db-auto-push.test.ts +11 -0
  45. package/src/cli/db-auto-push.ts +6 -2
  46. package/src/cli/db.test.ts +1 -1
  47. package/src/cli/db.ts +6 -6
  48. package/src/cli/dev-app-runner.ts +2 -1
  49. package/src/cli/dev-db-push.test.ts +6 -2
  50. package/src/cli/dev-schema-sync.ts +1 -1
  51. package/src/cli/dev.test.ts +10 -7
  52. package/src/cli/dev.ts +13 -11
  53. package/src/cli/ensure-drizzle-config.ts +4 -3
  54. package/src/cli/start.ts +2 -1
  55. package/src/client/create.ts +3 -3
  56. package/src/client/explain.test.ts +252 -0
  57. package/src/client/explain.ts +272 -0
  58. package/src/client/live.test.ts +44 -0
  59. package/src/client/notes-contract.test.ts +10 -0
  60. package/src/client/sse.ts +6 -2
  61. package/src/client/transport.test.ts +67 -0
  62. package/src/client/transport.ts +24 -40
  63. package/src/client/types.ts +17 -6
  64. package/src/client-react/browser.test.ts +23 -0
  65. package/src/client-react/live-resource.ts +6 -2
  66. package/src/client-react/use-live-query.ts +1 -1
  67. package/src/compiler/flow-path.test.ts +1 -0
  68. package/src/compiler/flow-path.ts +1 -1
  69. package/src/compiler/generate-adopt.test.ts +55 -1
  70. package/src/compiler/generate-adopt.ts +111 -21
  71. package/src/compiler/response.ts +17 -27
  72. package/src/config/index.ts +6 -4
  73. package/src/console/server/invoke-user-flow.test.ts +8 -2
  74. package/src/console/server/invoke-user-flow.ts +12 -18
  75. package/src/console/server/security.gate.test.ts +1 -1
  76. package/src/console/ui-next/dist/assets/{access-page-BoC83Ubl.js → access-page-tIbsiphz.js} +1 -1
  77. package/src/console/ui-next/dist/assets/{agent-disclosure-CKjAEOqA.js → agent-disclosure-CSKumwS2.js} +1 -1
  78. package/src/console/ui-next/dist/assets/{cache-glyph-Ceaq9pYh.js → cache-glyph-BCC-DxKT.js} +1 -1
  79. package/src/console/ui-next/dist/assets/{call-pii-button-C4lmY7ck.js → call-pii-button-jOmYISdc.js} +1 -1
  80. package/src/console/ui-next/dist/assets/{collapsible-82y257sL.js → collapsible-DC2xNaAb.js} +1 -1
  81. package/src/console/ui-next/dist/assets/{duration-tone-W63jaMZ8.js → duration-tone-CwoV56jn.js} +1 -1
  82. package/src/console/ui-next/dist/assets/{flows-page-KFTFj2rK.js → flows-page-gT1lsWQK.js} +1 -1
  83. package/src/console/ui-next/dist/assets/{highlighted-json-yE9zNTqC.js → highlighted-json-C2GZEJNI.js} +1 -1
  84. package/src/console/ui-next/dist/assets/{http-method-CwFeFroN.js → http-method-BdYjcIrD.js} +1 -1
  85. package/src/console/ui-next/dist/assets/{index-r7xXt_VV.js → index-Cul17AcV.js} +3 -3
  86. package/src/console/ui-next/dist/assets/{observability-page-BDiliMiC.js → observability-page-oY9vdYBk.js} +1 -1
  87. package/src/console/ui-next/dist/assets/{replica-lag-DYDzWUFT.js → replica-lag-C4QdAF7J.js} +1 -1
  88. package/src/console/ui-next/dist/assets/{request-meta-CzrOfgiz.js → request-meta-C43DHyld.js} +1 -1
  89. package/src/console/ui-next/dist/assets/{store-page-CZC2cwaw.js → store-page-CL0D9dOq.js} +1 -1
  90. package/src/console/ui-next/dist/assets/{trace-detail-sheet-DgeeejW7.js → trace-detail-sheet-Dcpo4Us_.js} +1 -1
  91. package/src/console/ui-next/dist/assets/{tree-expand-toggle-CzGIyOPY.js → tree-expand-toggle-CKJTuv43.js} +1 -1
  92. package/src/console/ui-next/dist/assets/{units-page-DBiDCLIB.js → units-page-1PlT19ft.js} +1 -1
  93. package/src/console/ui-next/dist/assets/{vault-page-CcHsthPe.js → vault-page-BwZ9YjTW.js} +1 -1
  94. package/src/console/ui-next/dist/index.html +1 -1
  95. package/src/docker/docker.test.ts +3 -3
  96. package/src/docker/images-config.test.ts +4 -4
  97. package/src/docker/stack-id.test.ts +1 -1
  98. package/src/drivers/clock-postgres.test.ts +10 -2
  99. package/src/drivers/clock-postgres.ts +18 -2
  100. package/src/drivers/vault-driver-removal.test.ts +2 -2
  101. package/src/elements/channel/declare.ts +66 -3
  102. package/src/elements/channel/runtime.ts +9 -11
  103. package/src/elements/channel.test.ts +42 -0
  104. package/src/elements/channel.ts +4 -2
  105. package/src/elements/clock/reconcile.ts +45 -24
  106. package/src/elements/clock.test.ts +33 -0
  107. package/src/elements/store/emit-drizzle.ts +285 -65
  108. package/src/elements/store/files-errors.test.ts +149 -0
  109. package/src/elements/store/files-errors.ts +189 -0
  110. package/src/elements/store/kv-errors.test.ts +98 -0
  111. package/src/elements/store/kv-errors.ts +139 -0
  112. package/src/elements/store/load-plugin-tables.ts +1 -1
  113. package/src/elements/store/prepare-row.test.ts +57 -4
  114. package/src/elements/store/resource.ts +11 -7
  115. package/src/elements/store/runtime.ts +18 -13
  116. package/src/elements/store/schema-decl.test.ts +178 -0
  117. package/src/elements/store/sql-errors.test.ts +197 -0
  118. package/src/elements/store/sql-errors.ts +294 -0
  119. package/src/elements/store/sql-session.test.ts +52 -0
  120. package/src/elements/store/sql-session.ts +50 -2
  121. package/src/elements/store/store-errors.ts +47 -0
  122. package/src/elements/store/table.ts +8 -6
  123. package/src/http.ts +9 -1
  124. package/src/i18n/catalogs/ar.ts +18 -0
  125. package/src/i18n/catalogs/en.ts +18 -0
  126. package/src/index.ts +9 -1
  127. package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
  128. package/src/kernel/app.ts +40 -34
  129. package/src/kernel/auto-registry.test.ts +26 -1
  130. package/src/kernel/boot.ts +2 -2
  131. package/src/kernel/boundary-contract.ts +6 -1
  132. package/src/kernel/builtin-errors.test.ts +117 -0
  133. package/src/kernel/builtin-errors.ts +129 -0
  134. package/src/kernel/call.test.ts +182 -0
  135. package/src/kernel/errors-vault.ts +16 -0
  136. package/src/kernel/errors.registry.test.ts +7 -0
  137. package/src/kernel/errors.ts +97 -24
  138. package/src/kernel/fail-helpers.ts +34 -0
  139. package/src/kernel/flow-units.ts +3 -3
  140. package/src/kernel/fx.test.ts +8 -0
  141. package/src/kernel/fx.ts +24 -8
  142. package/src/kernel/index.ts +12 -1
  143. package/src/kernel/mutation-id.ts +8 -0
  144. package/src/kernel/plugin.ts +4 -3
  145. package/src/kernel/project-out.test.ts +176 -0
  146. package/src/kernel/project-out.ts +91 -0
  147. package/src/kernel/realtime-bind.ts +2 -3
  148. package/src/kernel/router/linear.ts +12 -6
  149. package/src/kernel/router.test.ts +13 -0
  150. package/src/plugins/magic-link.ts +25 -24
  151. package/src/plugins/otp.ts +35 -24
  152. package/src/plugins/two-factor.ts +15 -0
  153. package/src/runs/duckdb.test.ts +2 -2
  154. package/src/runtime/dev-request-log.ts +29 -11
  155. package/src/term.test.ts +76 -0
  156. package/src/term.ts +166 -3
@@ -464,6 +464,46 @@ export default defineConfig({
464
464
  }
465
465
  });
466
466
 
467
+ test("applyAiSetup writes src/core/ai.ts when the barrel is src/core/index.ts", () => {
468
+ const dir = mkdtempSync(join(tmpdir(), "oke-ai-setup-index-"));
469
+ try {
470
+ writeFileSync(
471
+ join(dir, "oke.config.ts"),
472
+ `import { defineConfig } from "okengine/config";
473
+ export default defineConfig({
474
+ drivers: {
475
+ channel: { email: { dev: "console", test: "console", prod: "console" } },
476
+ },
477
+ });
478
+ `,
479
+ "utf8",
480
+ );
481
+ mkdirSync(join(dir, "src", "core"), { recursive: true });
482
+ writeFileSync(join(dir, "src", "core", "index.ts"), `export * from "./store.ts";\n`, "utf8");
483
+ writeFileSync(
484
+ join(dir, "src", "app.ts"),
485
+ `import "@/core";\nexport const app = {};\n`,
486
+ "utf8",
487
+ );
488
+
489
+ applyAiSetup(dir, {
490
+ driver: "openai-compatible",
491
+ provider: "openrouter",
492
+ chatModel: "openrouter/free",
493
+ });
494
+
495
+ const index = readFileSync(join(dir, "src", "core", "index.ts"), "utf8");
496
+ expect(index).not.toContain("ai.model");
497
+ expect(index).toContain('export * from "./ai.ts"');
498
+ const aiTs = readFileSync(join(dir, "src", "core", "ai.ts"), "utf8");
499
+ expect(aiTs).toContain("smart");
500
+ const app = readFileSync(join(dir, "src", "app.ts"), "utf8");
501
+ expect(app).not.toContain("./core/ai");
502
+ } finally {
503
+ rmSync(dir, { recursive: true, force: true });
504
+ }
505
+ });
506
+
467
507
  test("applyAiSetup writes API token to .env.local and vault.secret into core", () => {
468
508
  const dir = mkdtempSync(join(tmpdir(), "oke-ai-setup-key-"));
469
509
  try {
@@ -434,8 +434,8 @@ export function ensureAiApiKeyVaultSecret(cwd: string, apiKeyEnv: string): void
434
434
 
435
435
  /**
436
436
  * Write AI model declarations into `src/core/ai.ts` when that split exists
437
- * (so a thin `src/core.ts` barrel stays a re-export), else `src/core.ts`,
438
- * else legacy `src/core/index.ts` + sidecar.
437
+ * (so a thin `src/core.ts` / `src/core/index.ts` barrel stays a re-export),
438
+ * else `src/core.ts`.
439
439
  *
440
440
  * @param cwd - Project root
441
441
  * @param input - Setup choices
@@ -445,25 +445,23 @@ function writeAiModels(cwd: string, input: AiSetupApplyInput): string {
445
445
  const rendered = renderAiTs(input);
446
446
  const coreAiPath = join(cwd, "src", "core", "ai.ts");
447
447
  const coreTsPath = join(cwd, "src", "core.ts");
448
- const legacyIndex = join(cwd, "src", "core", "index.ts");
449
-
450
- if (existsSync(coreAiPath)) {
451
- const existing = readFileSync(coreAiPath, "utf8");
452
- writeFileSync(coreAiPath, resolveAiCoreSource(existing, rendered), "utf8");
448
+ const coreIndexPath = join(cwd, "src", "core", "index.ts");
449
+ const splitLayout =
450
+ existsSync(coreAiPath) || (!existsSync(coreTsPath) && existsSync(coreIndexPath));
451
+
452
+ if (splitLayout) {
453
+ mkdirSync(dirname(coreAiPath), { recursive: true });
454
+ const existing = existsSync(coreAiPath) ? readFileSync(coreAiPath, "utf8") : "";
455
+ writeFileSync(
456
+ coreAiPath,
457
+ existing ? resolveAiCoreSource(existing, rendered) : rendered,
458
+ "utf8",
459
+ );
453
460
  ensureCoreBarrelExportsAi(cwd);
454
461
  ensureCoreImported(cwd);
455
462
  return coreAiPath;
456
463
  }
457
464
 
458
- // Folder layout still in the wild — keep writing a sidecar.
459
- if (!existsSync(coreTsPath) && existsSync(legacyIndex)) {
460
- const aiTsPath = join(cwd, "src", "core", "ai.ts");
461
- mkdirSync(dirname(aiTsPath), { recursive: true });
462
- writeFileSync(aiTsPath, rendered, "utf8");
463
- ensureLegacyAiImported(cwd);
464
- return aiTsPath;
465
- }
466
-
467
465
  mkdirSync(dirname(coreTsPath), { recursive: true });
468
466
  if (existsSync(coreTsPath)) {
469
467
  const existing = readFileSync(coreTsPath, "utf8");
@@ -625,16 +623,25 @@ export function ensureNamedOkengineImport(source: string, name: string): string
625
623
  }
626
624
 
627
625
  /**
628
- * Keep a `src/core.ts` barrel re-exporting `./core/ai.ts` after a split write.
626
+ * Keep a core barrel re-exporting the AI sidecar after a split write.
627
+ *
628
+ * `src/core.ts` → `./core/ai.ts`; `src/core/index.ts` → `./ai.ts`.
629
629
  *
630
630
  * @param cwd - Project root
631
631
  */
632
632
  function ensureCoreBarrelExportsAi(cwd: string): void {
633
633
  const coreTsPath = join(cwd, "src", "core.ts");
634
- if (!existsSync(coreTsPath)) return;
635
- const src = readFileSync(coreTsPath, "utf8");
636
- if (/from\s+["']\.\/core\/ai/.test(src)) return;
637
- writeFileSync(coreTsPath, `${src.trimEnd()}\nexport * from "./core/ai.ts";\n`, "utf8");
634
+ if (existsSync(coreTsPath)) {
635
+ const src = readFileSync(coreTsPath, "utf8");
636
+ if (/from\s+["']\.\/core\/ai(?:\.ts)?["']/.test(src)) return;
637
+ writeFileSync(coreTsPath, `${src.trimEnd()}\nexport * from "./core/ai.ts";\n`, "utf8");
638
+ return;
639
+ }
640
+ const coreIndexPath = join(cwd, "src", "core", "index.ts");
641
+ if (!existsSync(coreIndexPath)) return;
642
+ const src = readFileSync(coreIndexPath, "utf8");
643
+ if (/from\s+["']\.\/ai(?:\.ts)?["']/.test(src)) return;
644
+ writeFileSync(coreIndexPath, `${src.trimEnd()}\nexport * from "./ai.ts";\n`, "utf8");
638
645
  }
639
646
 
640
647
  /**
@@ -656,28 +663,3 @@ function ensureCoreImported(cwd: string): void {
656
663
  }
657
664
  writeFileSync(appPath, `import "@/core";\n${src}`, "utf8");
658
665
  }
659
-
660
- /**
661
- * Legacy `src/core/*` layout — side-effect import the AI sidecar.
662
- *
663
- * @param cwd - Project root
664
- */
665
- function ensureLegacyAiImported(cwd: string): void {
666
- const candidates: ReadonlyArray<{ readonly rel: string; readonly importLine: string }> = [
667
- { rel: "src/app.ts", importLine: `import "./core/ai";\n` },
668
- { rel: "src/core/index.ts", importLine: `import "./ai";\n` },
669
- ];
670
- for (const { rel, importLine } of candidates) {
671
- const path = join(cwd, rel);
672
- if (!existsSync(path)) continue;
673
- const src = readFileSync(path, "utf8");
674
- if (
675
- /from\s+["']\.\/(?:core\/)?ai["']/.test(src) ||
676
- /import\s+["']\.\/(?:core\/)?ai["']/.test(src)
677
- ) {
678
- return;
679
- }
680
- writeFileSync(path, `${importLine}${src}`, "utf8");
681
- return;
682
- }
683
- }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `oke build` regenerates `src/flows/generated.ts` before bundling — real
2
+ * `oke build` regenerates `src/flows/index.ts` before bundling — real
3
3
  * before/after proof, not an assumption about the wiring.
4
4
  */
5
5
 
@@ -16,13 +16,13 @@ async function makeUnit(root: string, name: string): Promise<void> {
16
16
  }
17
17
 
18
18
  describe("oke build — .adopt() barrel regeneration", () => {
19
- test("real before/after: no generated.ts before build, real barrel content after", async () => {
19
+ test("real before/after: no index.ts before build, real barrel content after", async () => {
20
20
  const root = await mkdtemp(join(tmpdir(), "oke-build-adopt-"));
21
21
  try {
22
22
  await makeUnit(root, "main");
23
23
  await makeUnit(root, "notes");
24
24
 
25
- const generatedPath = join(root, "src/flows/generated.ts");
25
+ const generatedPath = join(root, "src/flows/index.ts");
26
26
  await expect(readFile(generatedPath, "utf8")).rejects.toThrow();
27
27
 
28
28
  const code = await runBuild({
package/src/cli/build.ts CHANGED
@@ -18,16 +18,16 @@ export interface BuildOptions {
18
18
  }) => Promise<{ success: boolean; logs: string }>;
19
19
  /**
20
20
  * Inject `.adopt()` barrel regeneration (tests). Default: real
21
- * `generateAdoptBarrel` + atomic write to `<rootDir>/src/flows/generated.ts`
22
- * (`generated.ts.tmp` → rename). Returns the unit names written, for the
21
+ * `generateAdoptBarrel` + atomic write to `<rootDir>/src/flows/index.ts`
22
+ * (`index.ts.tmp` → rename). Returns the unit names written, for the
23
23
  * status line.
24
24
  */
25
25
  readonly syncAdoptBarrel?: (rootDir: string) => Promise<readonly string[]>;
26
26
  }
27
27
 
28
28
  /**
29
- * Regenerate `src/flows/generated.ts` from every `src/flows/<unit>/index.ts`
30
- * unit folder — pre-step for `oke build` (mirrors `oke db`'s `schema.drizzle.ts`
29
+ * Regenerate `src/flows/index.ts` from every `src/flows/<unit>/` folder —
30
+ * pre-step for `oke build` (mirrors `oke db`'s `drizzle/`
31
31
  * pre-step). A real file on disk, not a virtual module: identical resolution
32
32
  * under `oke dev`'s runtime `import()` and `oke build`'s `Bun.build()`
33
33
  * (investigated — a virtual-module Bun plugin does not resolve consistently
@@ -76,7 +76,7 @@ export async function runBuild(options: BuildOptions = {}): Promise<number> {
76
76
  try {
77
77
  const units = await syncAdoptBarrel(rootDir);
78
78
  write(
79
- `oke build: .adopt() barrel → src/flows/generated.ts (${units.length} unit${units.length === 1 ? "" : "s"})\n`,
79
+ `oke build: .adopt() barrel → src/flows/index.ts (${units.length} unit${units.length === 1 ? "" : "s"})\n`,
80
80
  );
81
81
  const result = await bundle({ entry, outdir, target: bunTarget });
82
82
  if (!result.success) {
@@ -17,7 +17,18 @@ describe("isDomainSchemaWatchPath", () => {
17
17
  expect(isDomainSchemaWatchPath("src/schema.decl.ts")).toBe(true);
18
18
  });
19
19
 
20
+ test("matches split declare modules under schema/", () => {
21
+ expect(isDomainSchemaWatchPath("db/schema/links.ts")).toBe(true);
22
+ expect(isDomainSchemaWatchPath("src/db/schema/daily.ts")).toBe(true);
23
+ expect(isDomainSchemaWatchPath("src/db/schema/index.ts")).toBe(true);
24
+ expect(isDomainSchemaWatchPath("src/db/schema/relations.ts")).toBe(true);
25
+ });
26
+
20
27
  test("ignores emit output (avoids push ↔ generated feedback loop)", () => {
28
+ expect(isDomainSchemaWatchPath("src/db/drizzle.ts")).toBe(false);
29
+ expect(isDomainSchemaWatchPath("drizzle.ts")).toBe(false);
30
+ expect(isDomainSchemaWatchPath("src/db/drizzle/index.ts")).toBe(false);
31
+ expect(isDomainSchemaWatchPath("src/db/drizzle/links.ts")).toBe(false);
21
32
  expect(isDomainSchemaWatchPath("src/db/schema.drizzle.ts")).toBe(false);
22
33
  expect(isDomainSchemaWatchPath("schema.drizzle.ts")).toBe(false);
23
34
  expect(isDomainSchemaWatchPath("flows/notes/schema.drizzle.tsx")).toBe(false);
@@ -11,8 +11,8 @@ export const DB_AUTO_PUSH_DEBOUNCE_MS = 300;
11
11
  * Whether a watched path should trigger `oke db push`.
12
12
  *
13
13
  * Inputs only: declaration / drizzle config / app entry. Never the emit
14
- * output (`schema.drizzle.ts`) — push rewrites that file every run, so
15
- * watching it loops `oke db push` forever under `oke dev`.
14
+ * output (`src/db/drizzle/`) — push rewrites those files every run, so
15
+ * watching them loops `oke db push` forever under `oke dev`.
16
16
  *
17
17
  * @param filename - Relative path from the watcher (may be undefined)
18
18
  */
@@ -21,8 +21,10 @@ export function isDomainSchemaWatchPath(filename: string | null | undefined): bo
21
21
  const normalized = filename.replace(/\\/g, "/");
22
22
  const base = basename(normalized);
23
23
  // Emit output — never a watch trigger (feedback loop with `oke db push`).
24
+ if (base === "drizzle.ts" || base === "drizzle.tsx") return false;
24
25
  if (base === "schema.drizzle.ts" || base === "schema.drizzle.tsx") return false;
25
26
  if (base === "schema.generated.ts" || base === "schema.generated.tsx") return false;
27
+ if (/(^|\/)drizzle\/.+\.tsx?$/.test(normalized)) return false;
26
28
  if (base === "schema.ts" || base === "schema.tsx") return true;
27
29
  if (base === "schema.decl.ts" || base === "schema.decl.tsx") return true;
28
30
  if (base === "drizzle.config.ts" || base === "drizzle.config.js") return true;
@@ -30,6 +32,8 @@ export function isDomainSchemaWatchPath(filename: string | null | undefined): bo
30
32
  if (base === "app.ts" || base === "app.tsx") return true;
31
33
  // Nested domain schemas commonly live under src/**/schema.ts (not emit output).
32
34
  if (/(^|\/)schema(\.decl)?\.tsx?$/.test(normalized)) return true;
35
+ // Split declare modules: `src/db/schema/links.ts` next to `schema/index.ts`.
36
+ if (/(^|\/)schema\/.+\.tsx?$/.test(normalized)) return true;
33
37
  return false;
34
38
  }
35
39
 
@@ -327,7 +327,7 @@ export const app = oke({ name: "plugin-only" }).plug(
327
327
  const emitted = await emitAbstractSchemaPrestep(dir, loaded.config, () => {}, "test");
328
328
  expect(emitted).toBe(true);
329
329
 
330
- const generated = await readFile(join(dir, "src", "db", "schema.drizzle.ts"), "utf8");
330
+ const generated = await readFile(join(dir, "src", "db", "drizzle", "metrics_daily.ts"), "utf8");
331
331
  expect(generated).toContain('pgTable("metrics_daily"');
332
332
  // Postgres dialect maps abstract integer → bigint (32-bit INTEGER is too narrow).
333
333
  expect(generated).toContain('bigint("hits", { mode: "number" })');
package/src/cli/db.ts CHANGED
@@ -3,8 +3,8 @@
3
3
  * plus explicit seed runs.
4
4
  *
5
5
  * Distinct from `oke schema generate` (core/plugin stub tables).
6
- * When abstract decls exist (`src/db/schema.decl.ts`), emits Drizzle first —
7
- * not a third schema CLI.
6
+ * When abstract decls exist (`src/db/schema.ts` or `src/db/schema/index.ts`),
7
+ * emits Drizzle first — not a third schema CLI.
8
8
  */
9
9
 
10
10
  import { resolve } from "node:path";
@@ -279,7 +279,7 @@ export async function runDb(sub: DbSubcommand, options: DbOptions = {}): Promise
279
279
  }
280
280
 
281
281
  /**
282
- * Emit `schema.drizzle.ts` from abstract decls + live plugged plugin tables.
282
+ * Emit `drizzle/` from abstract decls + live plugged plugin tables.
283
283
  *
284
284
  * @param cwd - Project root
285
285
  * @param config - Loaded oke config
@@ -596,9 +596,9 @@ export async function dbCli(args: readonly string[]): Promise<number> {
596
596
  console.log(`oke db push|generate|migrate|seed|studio|search-backfill [--config|-c] [--env name] [--force]
597
597
 
598
598
  Domain schema sync via drizzle-kit, plus explicit seed and hybrid-search backfill.
599
- When src/db/schema.decl.ts and/or a plugged app entry exists, emits
600
- schema.drizzle.ts from store.schema.table + live plugin .table()
601
- contributions — then runs drizzle-kit.
599
+ When src/db/schema.ts or src/db/schema/index.ts and/or a plugged app
600
+ entry exists, emits src/db/drizzle/ from store.schema.table + live plugin
601
+ .table() contributions — then runs drizzle-kit.
602
602
  Hand-written src/schema.ts remains supported (emit skipped if nothing to emit).
603
603
  Not the same as \`oke schema generate\` (core/plugin stub tables).
604
604
 
@@ -90,7 +90,8 @@ try {
90
90
  // `bun --hot` keeps the watcher alive on an uncaught throw, so `oke dev`
91
91
  // would wait the full ready timeout after VaultBootError. Exit so the
92
92
  // parent sees a dead child immediately.
93
- console.error(err);
93
+ const { formatFatalError } = await import("../term.ts");
94
+ console.error(formatFatalError(err));
94
95
  process.exit(1);
95
96
  }
96
97
  const handle = createBunRuntime().serve(mod.app, {
@@ -169,7 +169,7 @@ export const entries = pgTable("entries", {
169
169
  expect(pushes.length).toBeGreaterThan(before);
170
170
  });
171
171
 
172
- test("schema.drizzle.ts change does not re-trigger dbPush", async () => {
172
+ test("generated drizzle.ts change does not re-trigger dbPush", async () => {
173
173
  const cwd = await scaffoldProject();
174
174
  const pushes: string[] = [];
175
175
  const watcher = controlledWatcher();
@@ -221,7 +221,11 @@ export const entries = pgTable("entries", {
221
221
  const before = pushes.length;
222
222
  expect(before).toBeGreaterThanOrEqual(1);
223
223
 
224
- // Simulate emit writing schema.drizzle.ts after push — must not loop.
224
+ // Simulate emit writing drizzle.ts after push — must not loop.
225
+ watcher.change("drizzle.ts");
226
+ watcher.change("src/db/drizzle.ts");
227
+ watcher.change("src/db/drizzle/index.ts");
228
+ watcher.change("src/db/drizzle/links.ts");
225
229
  watcher.change("schema.drizzle.ts");
226
230
  watcher.change("src/db/schema.drizzle.ts");
227
231
  watcher.change("schema.generated.ts");
@@ -25,7 +25,7 @@ import { loadOkeConfig, resolveImages } from "./load-config.ts";
25
25
  export interface DevSchemaSyncOptions {
26
26
  /** Write output (default: stdout). */
27
27
  readonly write?: (text: string) => void;
28
- /** Skip emitting `schema.drizzle.ts` (tests). */
28
+ /** Skip emitting `drizzle/` (tests). */
29
29
  readonly skipEmit?: boolean;
30
30
  /** Injectable push (tests). Default: `runDb("push", ...)`. */
31
31
  readonly pushFn?: (cwd: string, env: ConfigEnv) => Promise<number>;
@@ -706,7 +706,7 @@ describe("oke dev hot reload", () => {
706
706
  });
707
707
 
708
708
  describe("oke dev syncAdoptBarrel atomic write", () => {
709
- test("default path regenerates generated.ts and leaves no .tmp behind", async () => {
709
+ test("default path regenerates index.ts and leaves no .tmp behind", async () => {
710
710
  const dir = await mkdtemp(join(tmpdir(), "oke-dev-adopt-atomic-"));
711
711
  await mkdir(join(dir, "src/flows/notes"), { recursive: true });
712
712
  await Bun.write(join(dir, "src/flows/notes/index.ts"), "export {};\n");
@@ -791,9 +791,12 @@ describe("oke dev syncAdoptBarrel atomic write", () => {
791
791
  });
792
792
 
793
793
  describe("isFlowsTreeWatchPath", () => {
794
- test("flows tree files trigger regen; generated.ts does not", () => {
794
+ test("flows tree files trigger regen; adopt barrel does not", () => {
795
795
  expect(isFlowsTreeWatchPath("flows/notes/[id]/get.ts")).toBe(true);
796
796
  expect(isFlowsTreeWatchPath("flows\\notes\\list.ts")).toBe(true);
797
+ expect(isFlowsTreeWatchPath("flows/notes/index.ts")).toBe(true);
798
+ expect(isFlowsTreeWatchPath("flows/index.ts")).toBe(false);
799
+ expect(isFlowsTreeWatchPath("flows/index.ts.tmp")).toBe(false);
797
800
  expect(isFlowsTreeWatchPath("flows/generated.ts")).toBe(false);
798
801
  expect(isFlowsTreeWatchPath("flows/generated.ts.tmp")).toBe(false);
799
802
  expect(isFlowsTreeWatchPath("core.ts")).toBe(false);
@@ -953,7 +956,7 @@ describe("oke dev Docker-first", () => {
953
956
  });
954
957
  });
955
958
 
956
- describe("oke dev schema.decl sync framing", () => {
959
+ describe("oke dev schema declare sync framing", () => {
957
960
  let session: DevSession | undefined;
958
961
 
959
962
  afterEach(() => {
@@ -981,7 +984,7 @@ describe("oke dev schema.decl sync framing", () => {
981
984
  await Bun.write(join(dir, "src/app.ts"), "export {}\n");
982
985
  // Genuine definition error: `.references()` target is undefined → emit throws.
983
986
  await Bun.write(
984
- join(dir, "src/db/schema.decl.ts"),
987
+ join(dir, "src/db/schema.ts"),
985
988
  `import { store, field } from ${JSON.stringify(OKE_INDEX)};
986
989
 
987
990
  export const authors = store.schema.table("authors", {
@@ -1024,8 +1027,8 @@ export const posts = store.schema.table("posts", {
1024
1027
  session = result.session;
1025
1028
 
1026
1029
  const plain = writes.join("").replace(/\x1b\[[0-9;]*m/g, "");
1027
- expect(plain).toContain("schema.decl.ts has an error");
1028
- expect(plain).toMatch(/●\s*schema\.decl\.ts has an error/);
1030
+ expect(plain).toContain("schema.ts has an error");
1031
+ expect(plain).toMatch(/●\s*schema\.ts has an error/);
1029
1032
  expect(plain).not.toContain("oke db push (dev) skipped");
1030
1033
  }, 60_000);
1031
1034
 
@@ -1065,7 +1068,7 @@ export const posts = store.schema.table("posts", {
1065
1068
 
1066
1069
  const plain = writes.join("").replace(/\x1b\[[0-9;]*m/g, "");
1067
1070
  expect(plain).toContain("oke db push (dev) skipped — docker mode: oke.config.ts not found");
1068
- expect(plain).not.toContain("schema.decl.ts has an error");
1071
+ expect(plain).not.toContain("schema.ts has an error");
1069
1072
  }, 60_000);
1070
1073
  });
1071
1074
 
package/src/cli/dev.ts CHANGED
@@ -27,6 +27,7 @@ import {
27
27
  writeStackCredentialsCache,
28
28
  type DeriveOptions,
29
29
  } from "../docker/index.ts";
30
+ import { isAdoptBarrelRelPath } from "../compiler/generate-adopt.ts";
30
31
  import type { Manifest } from "../manifest/types.ts";
31
32
  import type { McpContext } from "../mcp/tools.ts";
32
33
  import { APP_PORT, CONSOLE_PORT, DOCS_MCP_PORT, MCP_PORT } from "../runtime/types.ts";
@@ -183,9 +184,9 @@ export interface DevOptions {
183
184
  readonly onDbAutoPush?: (filename: string) => void;
184
185
  /**
185
186
  * Injectable `.adopt()` barrel regeneration (tests). Default: real
186
- * `generateAdoptBarrel` + atomic write to `<cwd>/src/flows/generated.ts`
187
- * (`generated.ts.tmp` → rename). Runs at session start and again on
188
- * `src/flows/**` changes except `generated.ts` itself. Write-if-changed
187
+ * `generateAdoptBarrel` + atomic write to `<cwd>/src/flows/index.ts`
188
+ * (`index.ts.tmp` → rename). Runs at session start and again on
189
+ * `src/flows/**` changes except `index.ts` itself. Write-if-changed
189
190
  * so the `src/` watcher + bun `--hot` do not loop.
190
191
  *
191
192
  * @param cwd - Project root
@@ -370,7 +371,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
370
371
  const cwd = options.cwd ?? process.cwd();
371
372
 
372
373
  // One-shot `.adopt()` barrel regen for the session — real file on disk
373
- // (`src/flows/generated.ts`), written atomically (tmp → rename) before the
374
+ // (`src/flows/index.ts`), written atomically (tmp → rename) before the
374
375
  // entry is resolved/imported below so a freshly-added flows unit is
375
376
  // adoptable without a hand edit. Best-effort like `syncDevSchema` below:
376
377
  // a project with no `src/flows` yet (or a synthetic test tree) never
@@ -1036,7 +1037,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
1036
1037
  }
1037
1038
 
1038
1039
  // One-shot schema sync for the compose (`dev`) profile: emits
1039
- // schema.drizzle.ts for the active dialect, then pushes via drizzle-kit.
1040
+ // drizzle/ for the active dialect, then pushes via drizzle-kit.
1040
1041
  let schemaPushOk = options.noDbPush === true;
1041
1042
  if (!options.noDbPush) {
1042
1043
  const { syncDevSchema } = await import("./dev-schema-sync.ts");
@@ -1576,7 +1577,7 @@ function isBenignDevSchemaEnvSkip(message: string): boolean {
1576
1577
  }
1577
1578
 
1578
1579
  /**
1579
- * Whether a caught sync/push error is a schema.decl definition failure
1580
+ * Whether a caught sync/push error is a schema declare definition failure
1580
1581
  * (import evaluation, emit, duplicate tables, bad `.references()`, …).
1581
1582
  *
1582
1583
  * Message text alone is insufficient — emit-time bugs often look like bare
@@ -1584,7 +1585,7 @@ function isBenignDevSchemaEnvSkip(message: string): boolean {
1584
1585
  * emit frames.
1585
1586
  *
1586
1587
  * @param err - Caught value
1587
- * @param declarePath - Resolved schema.decl path for this project
1588
+ * @param declarePath - Resolved schema declare path for this project
1588
1589
  */
1589
1590
  function isSchemaDeclDefinitionError(err: unknown, declarePath: string): boolean {
1590
1591
  const msg = err instanceof Error ? err.message : String(err);
@@ -1600,15 +1601,15 @@ function isSchemaDeclDefinitionError(err: unknown, declarePath: string): boolean
1600
1601
 
1601
1602
  /**
1602
1603
  * True when a `src/` watcher event is a flows tree change that should
1603
- * regenerate `generated.ts` (never the generated file itself).
1604
+ * regenerate `src/flows/index.ts` (never the barrel itself, nor a leftover
1605
+ * `generated.ts`). Unit `index.ts` files still trigger regen.
1604
1606
  *
1605
1607
  * @param rel - Watcher filename (POSIX or Windows)
1606
1608
  */
1607
1609
  export function isFlowsTreeWatchPath(rel: string): boolean {
1608
1610
  const posix = rel.replace(/\\/g, "/");
1609
1611
  if (!posix.includes("flows/")) return false;
1610
- const base = posix.split("/").pop() ?? posix;
1611
- if (base === "generated.ts" || base === "generated.ts.tmp") return false;
1612
+ if (isAdoptBarrelRelPath(posix)) return false;
1612
1613
  return true;
1613
1614
  }
1614
1615
 
@@ -1638,7 +1639,8 @@ async function formatDevSchemaSyncFailure(
1638
1639
  const { resolveEmitPaths } = await import("../elements/store/emit-drizzle.ts");
1639
1640
  const { declarePath } = resolveEmitPaths(cwd);
1640
1641
  if (isSchemaDeclDefinitionError(err, declarePath)) {
1641
- return formatStatusLine(`schema.decl.ts has an error — ${msg}`, color, "error");
1642
+ const declareName = declarePath.split(/[/\\]/).pop() ?? "schema.ts";
1643
+ return formatStatusLine(`${declareName} has an error — ${msg}`, color, "error");
1642
1644
  }
1643
1645
 
1644
1646
  const label = context === "watch" ? "oke db push (schema change)" : "oke db push (dev)";
@@ -12,8 +12,9 @@ const DRIZZLE_CONFIG_TEMPLATE = `import { defineConfig } from "drizzle-kit";
12
12
  * Domain schema sync for \`oke db push|generate|migrate\`.
13
13
  *
14
14
  * SQL is PostgreSQL-only (postgres / pglite). Domain tables come from
15
- * \`src/db/schema.decl.ts\` → \`src/db/schema.drizzle.ts\` (emitted as a
16
- * pre-step of \`oke db\` / \`oke dev\`).
15
+ * \`src/db/schema.ts\` (single file) or \`src/db/schema/index.ts\`
16
+ * (folder) → \`src/db/drizzle/\` (emitted as a pre-step of \`oke db\` /
17
+ * \`oke dev\`). \`schema.ts\` wins when both exist.
17
18
  *
18
19
  * System / auth / plugin stubs are separate: \`oke schema generate\` →
19
20
  * \`.oke/schema/oke.ts\` (gitignored with \`.oke/\`). Re-run after adding plugins.
@@ -22,7 +23,7 @@ const DRIZZLE_CONFIG_TEMPLATE = `import { defineConfig } from "drizzle-kit";
22
23
  */
23
24
  export default defineConfig({
24
25
  dialect: "postgresql",
25
- schema: "./src/db/schema.drizzle.ts",
26
+ schema: "./src/db/drizzle/index.ts",
26
27
  out: "./src/db/migrations",
27
28
  // Domain tables live in public. Schema \`oke\` (RLS) and \`oke_console\` are engine-owned.
28
29
  schemaFilter: ["public"],
package/src/cli/start.ts CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { resolve } from "node:path";
6
6
  import { APP_PORT } from "../runtime/types.ts";
7
+ import { formatFatalError } from "../term.ts";
7
8
 
8
9
  /** Options for {@link runStart}. */
9
10
  export interface StartOptions {
@@ -77,7 +78,7 @@ export async function runStart(options: StartOptions = {}): Promise<number> {
77
78
  await import(entry);
78
79
  return 0;
79
80
  } catch (err) {
80
- console.error(err instanceof Error ? err.message : String(err));
81
+ console.error(formatFatalError(err));
81
82
  return 1;
82
83
  }
83
84
  }
@@ -194,14 +194,14 @@ export function flattenRoutes(
194
194
  function proxy(transport: Transport, path: readonly string[], ctx: ProxyCtx): unknown {
195
195
  const invoke = async (input?: unknown, callOpts?: CallOpts): Promise<ClientResult> => {
196
196
  if (path.length < 2) {
197
+ const message = `Incomplete path: api.${path.join(".") || "?"}(…)`;
197
198
  return attachPager(
198
199
  {
199
200
  data: null,
200
201
  error: {
201
202
  code: "TransportError" as const,
202
- data: {
203
- message: `Incomplete path: api.${path.join(".") || "?"}(…)`,
204
- },
203
+ message,
204
+ data: { message },
205
205
  },
206
206
  },
207
207
  (nextInput) => invoke(nextInput, callOpts),