okengine 0.9.1 → 0.10.1

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 (175) hide show
  1. package/AGENTS.md +1 -1
  2. package/manifest.v1.schema.json +9 -1
  3. package/package.json +3 -3
  4. package/site/content/docs/console/runs.mdx +10 -0
  5. package/site/content/docs/console/traces.mdx +4 -0
  6. package/site/content/docs/elements/ai.mdx +54 -32
  7. package/site/content/docs/elements/channel.mdx +97 -9
  8. package/site/content/docs/elements/clock.mdx +1 -0
  9. package/site/content/docs/elements/flow.mdx +42 -8
  10. package/site/content/docs/get-started/basic-usage.mdx +5 -1
  11. package/site/content/docs/get-started/installation.mdx +9 -5
  12. package/site/content/docs/meta.json +2 -0
  13. package/site/content/docs/providers/azure-redis.mdx +85 -0
  14. package/site/content/docs/providers/cockroachdb.mdx +86 -0
  15. package/site/content/docs/providers/digitalocean-caching.mdx +88 -0
  16. package/site/content/docs/providers/dragonfly-cloud.mdx +82 -0
  17. package/site/content/docs/providers/elasticache.mdx +92 -0
  18. package/site/content/docs/providers/index.mdx +101 -0
  19. package/site/content/docs/providers/memorystore.mdx +89 -0
  20. package/site/content/docs/providers/meta.json +20 -0
  21. package/site/content/docs/providers/neon.mdx +115 -0
  22. package/site/content/docs/providers/redis-cloud.mdx +93 -0
  23. package/site/content/docs/providers/supabase.mdx +117 -0
  24. package/site/content/docs/providers/upstash.mdx +91 -0
  25. package/site/content/docs/providers/yugabytedb.mdx +95 -0
  26. package/site/content/docs/recipes/caddy.mdx +128 -0
  27. package/site/content/docs/recipes/cockroachdb.mdx +127 -0
  28. package/site/content/docs/recipes/dragonfly.mdx +125 -0
  29. package/site/content/docs/recipes/index.mdx +146 -0
  30. package/site/content/docs/recipes/llama-cpp.mdx +153 -0
  31. package/site/content/docs/recipes/mailpit.mdx +136 -0
  32. package/site/content/docs/recipes/meilisearch.mdx +139 -0
  33. package/site/content/docs/recipes/meta.json +31 -0
  34. package/site/content/docs/recipes/ollama.mdx +142 -0
  35. package/site/content/docs/recipes/openbao.mdx +141 -0
  36. package/site/content/docs/recipes/pgdog.mdx +137 -0
  37. package/site/content/docs/recipes/postgres.mdx +136 -0
  38. package/site/content/docs/recipes/redis.mdx +130 -0
  39. package/site/content/docs/recipes/rustfs.mdx +148 -0
  40. package/site/content/docs/recipes/sglang.mdx +105 -0
  41. package/site/content/docs/recipes/supabase-docker.mdx +145 -0
  42. package/site/content/docs/recipes/timescale.mdx +129 -0
  43. package/site/content/docs/recipes/traefik.mdx +136 -0
  44. package/site/content/docs/recipes/valkey.mdx +121 -0
  45. package/site/content/docs/recipes/vllm.mdx +106 -0
  46. package/site/content/docs/recipes/yugabytedb.mdx +135 -0
  47. package/site/content/docs/reference/cli.md +42 -6
  48. package/site/content/docs/reference/configuration.mdx +5 -3
  49. package/site/content/docs/reference/environment-variables.mdx +12 -12
  50. package/site/content/docs/reference/fx.mdx +38 -4
  51. package/src/cli/ai-setup/ai-setup.test.ts +32 -12
  52. package/src/cli/ai-setup/apply.ts +65 -23
  53. package/src/cli/ai-setup/catalog.ts +1316 -35
  54. package/src/cli/ai-setup/detect-ollama.ts +47 -0
  55. package/src/cli/ai-setup/index.ts +44 -7
  56. package/src/cli/ai-setup/prompts.ts +430 -486
  57. package/src/cli/ai-setup/recommend.test.ts +150 -50
  58. package/src/cli/ai-setup/recommend.ts +118 -101
  59. package/src/cli/ask-seed.test.ts +68 -0
  60. package/src/cli/ask-seed.ts +79 -0
  61. package/src/cli/db-seed-redact.test.ts +25 -0
  62. package/src/cli/db-seed.ts +36 -7
  63. package/src/cli/db.test.ts +1 -1
  64. package/src/cli/db.ts +2 -2
  65. package/src/cli/dev-app-runner.ts +6 -0
  66. package/src/cli/dev-controls.test.ts +107 -0
  67. package/src/cli/dev-controls.ts +354 -0
  68. package/src/cli/dev-schema-sync.ts +12 -4
  69. package/src/cli/dev.test.ts +50 -30
  70. package/src/cli/dev.ts +633 -66
  71. package/src/cli/docker-cli.test.ts +15 -0
  72. package/src/cli/drizzle-env.test.ts +40 -6
  73. package/src/cli/drizzle-env.ts +22 -1
  74. package/src/cli/ensure-drizzle-config.ts +6 -4
  75. package/src/cli/hero-meta.test.ts +13 -1
  76. package/src/cli/hero-meta.ts +28 -6
  77. package/src/cli/index.ts +5 -0
  78. package/src/cli/registry.ts +24 -0
  79. package/src/cli/replay.test.ts +105 -0
  80. package/src/cli/replay.ts +206 -0
  81. package/src/cli/start.ts +12 -1
  82. package/src/compiler/effects-infer.ts +37 -8
  83. package/src/compiler/extract.test.ts +125 -0
  84. package/src/compiler/extract.ts +31 -6
  85. package/src/config/index.ts +3 -3
  86. package/src/console/server/claim.ts +3 -1
  87. package/src/console/ui/dist/assets/{index-ClX0ZCe5.js → index-D-9KFce5.js} +2 -2
  88. package/src/console/ui/dist/assets/panel-overview-CW0CQEc2.js +1 -0
  89. package/src/console/ui/dist/assets/panel-runs-BxuyDI3x.js +1 -0
  90. package/src/console/ui/dist/assets/{panel-signals-Dsxdu_AR.js → panel-signals-BXofbj7v.js} +1 -1
  91. package/src/console/ui/dist/assets/{panel-store-hy7O8HOs.js → panel-store-B8mH-arU.js} +1 -1
  92. package/src/console/ui/dist/assets/{panel-traces-CBiAe5go.js → panel-traces-DSA-ZWnf.js} +1 -1
  93. package/src/console/ui/dist/index.html +1 -1
  94. package/src/console/ui/overview/slo.ts +31 -3
  95. package/src/console/ui/overview/types.ts +6 -0
  96. package/src/console/ui/runs/errors.test.ts +53 -0
  97. package/src/console/ui/runs/errors.ts +88 -0
  98. package/src/console/ui/runs/index.ts +8 -0
  99. package/src/console/ui/runs/search.ts +16 -0
  100. package/src/console/ui/shell/panels/runs/RunsPanel.tsx +72 -1
  101. package/src/docker/ai-model-status.test.ts +101 -0
  102. package/src/docker/ai-model-status.ts +276 -0
  103. package/src/docker/compose-health.test.ts +115 -0
  104. package/src/docker/compose-health.ts +234 -0
  105. package/src/docker/compose.ts +24 -8
  106. package/src/docker/derive.ts +35 -9
  107. package/src/docker/docker.test.ts +304 -12
  108. package/src/docker/helpers.ts +56 -0
  109. package/src/docker/index.ts +52 -0
  110. package/src/docker/ollama-pull.ts +232 -0
  111. package/src/docker/recipes/cockroach.ts +24 -0
  112. package/src/docker/recipes/index.ts +32 -0
  113. package/src/docker/recipes/llama-cpp.ts +273 -0
  114. package/src/docker/recipes/ollama.ts +19 -23
  115. package/src/docker/recipes/pgdog.ts +4 -1
  116. package/src/docker/recipes/postgres.ts +5 -18
  117. package/src/docker/recipes/sglang.ts +55 -0
  118. package/src/docker/recipes/supabase.ts +21 -0
  119. package/src/docker/recipes/timescale.ts +18 -0
  120. package/src/docker/recipes/vllm.ts +44 -0
  121. package/src/docker/recipes/yugabyte.ts +23 -0
  122. package/src/docker/stack-id.test.ts +22 -0
  123. package/src/docker/stack-id.ts +38 -4
  124. package/src/docker/types.ts +15 -0
  125. package/src/drivers/ai-ollama.integration.test.ts +7 -4
  126. package/src/drivers/signal-engine.ts +10 -0
  127. package/src/drivers/signal-postgres.ts +24 -2
  128. package/src/drivers/signal-types.ts +10 -0
  129. package/src/elements/channel/fallback.test.ts +91 -0
  130. package/src/elements/channel/hard-bounce.test.ts +82 -0
  131. package/src/elements/channel/injection.test.ts +153 -0
  132. package/src/elements/channel/locale-ar.test.ts +102 -0
  133. package/src/elements/channel/otp-delivery.ts +5 -2
  134. package/src/elements/channel/receipts-ledger.test.ts +100 -0
  135. package/src/elements/channel/runtime.ts +24 -1
  136. package/src/elements/channel/test-helpers.ts +60 -0
  137. package/src/elements/channel/whatsapp-compliance.test.ts +124 -0
  138. package/src/elements/store/cache.test.ts +28 -0
  139. package/src/elements/store/cache.ts +1 -1
  140. package/src/elements/store/emit-drizzle.ts +43 -9
  141. package/src/elements/store/runtime.ts +1 -1
  142. package/src/elements/store/schema-decl.test.ts +2 -0
  143. package/src/elements/store/schema-decl.ts +15 -0
  144. package/src/elements/store/seed.ts +1 -1
  145. package/src/index.ts +1 -0
  146. package/src/kernel/app.ts +131 -10
  147. package/src/kernel/boot-bind/signal.ts +6 -2
  148. package/src/kernel/boot-bind/store.ts +2 -1
  149. package/src/kernel/boot.test.ts +6 -5
  150. package/src/kernel/boot.ts +113 -10
  151. package/src/kernel/compensate.test.ts +131 -0
  152. package/src/kernel/correlation.test.ts +108 -0
  153. package/src/kernel/effect-timing.test.ts +82 -0
  154. package/src/kernel/effects-stamping.test.ts +207 -0
  155. package/src/kernel/errors.ts +11 -0
  156. package/src/kernel/flow.ts +38 -0
  157. package/src/kernel/fx-runs.test.ts +49 -0
  158. package/src/kernel/fx.test.ts +14 -0
  159. package/src/kernel/fx.ts +153 -14
  160. package/src/manifest/sql-resource.ts +41 -0
  161. package/src/manifest/types.ts +8 -2
  162. package/src/runs/collect.ts +3 -0
  163. package/src/runs/export-otlp.ts +64 -0
  164. package/src/runs/index.ts +16 -0
  165. package/src/runs/parquet.ts +9 -0
  166. package/src/runs/types.ts +5 -0
  167. package/src/runs/window.test.ts +68 -0
  168. package/src/runs/window.ts +148 -0
  169. package/src/runtime/boot-warn.test.ts +45 -0
  170. package/src/runtime/boot-warn.ts +17 -0
  171. package/src/runtime/dev-request-log.ts +5 -5
  172. package/src/term.test.ts +95 -10
  173. package/src/term.ts +369 -14
  174. package/src/console/ui/dist/assets/panel-overview-DONDzBd2.js +0 -1
  175. package/src/console/ui/dist/assets/panel-runs-D5zf-D9c.js +0 -1
@@ -15,6 +15,7 @@ import type {
15
15
  TemplateRef,
16
16
  FlowRef,
17
17
  } from "../manifest/types.ts";
18
+ import { sqlTableRef } from "../manifest/sql-resource.ts";
18
19
 
19
20
  /** Minimal ESTree-shaped nodes produced by oxc-parser. */
20
21
  export interface AstNode {
@@ -54,6 +55,7 @@ export interface InferBinding {
54
55
  | "template"
55
56
  | "flow"
56
57
  | "embed"
58
+ | "table"
57
59
  | "unknown";
58
60
  /** Resolved resource / name. */
59
61
  readonly ref: string;
@@ -103,9 +105,20 @@ const READ_METHODS = new Set([
103
105
  "findMany",
104
106
  "findFirst",
105
107
  "find",
108
+ "list",
106
109
  ]);
107
110
 
108
- const WRITE_METHODS = new Set(["insert", "set", "delete", "increment", "update", "upsert", "log"]);
111
+ const WRITE_METHODS = new Set([
112
+ "insert",
113
+ "set",
114
+ "delete",
115
+ "increment",
116
+ "update",
117
+ "upsert",
118
+ "log",
119
+ "put",
120
+ "putImage",
121
+ ]);
109
122
 
110
123
  /** Methods whose first argument is a table / collection identifier. */
111
124
  const TABLE_ARG_METHODS = new Set([
@@ -203,7 +216,7 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
203
216
  // Skip incomplete chains (`select` before `.from`, `insert` before table) —
204
217
  // the sibling call that carries the table arg records the real resource.
205
218
  const leaf = resolved.methods[resolved.methods.length - 1]!;
206
- const hasTable = tableFromStoreChain(call) !== undefined;
219
+ const hasTable = tableFromStoreChain(call, options.bindings) !== undefined;
207
220
  if (!hasTable && (leaf === "select" || leaf === "insert" || leaf === "update")) {
208
221
  continue;
209
222
  }
@@ -352,11 +365,14 @@ export function storeResourceFromCall(
352
365
  const storeBinding = resolveBinding(storeArg, bindings);
353
366
  const facet = storeBinding?.facet ?? "sql";
354
367
 
355
- const table = tableFromStoreChain(call);
368
+ const table = tableFromStoreChain(call, bindings);
356
369
 
357
370
  if (table) {
358
371
  return {
359
- resource: `${facet}:${table}` as ResourceRef,
372
+ // sql:<table> is the shared naming convention with the kernel's
373
+ // runtime capability gate (see ../manifest/sql-resource.ts) — table
374
+ // args only ever occur on sql-facet methods in practice.
375
+ resource: (facet === "sql" ? sqlTableRef(table) : `${facet}:${table}`) as ResourceRef,
360
376
  methods: chain.methods,
361
377
  };
362
378
  }
@@ -382,11 +398,21 @@ export function storeResourceFromCall(
382
398
  }
383
399
 
384
400
  /**
385
- * Walk an `fx.store(…).a().b(table)` chain and return the table identifier.
401
+ * Walk an `fx.store(…).a().b(table)` chain and return the declared table
402
+ * name — resolved through a registered `table` binding (the real string
403
+ * passed to `store.schema.table(name, …)`) when the argument is one, so a
404
+ * JS binding named differently from its declared table (`const notesTable
405
+ * = store.schema.table("notes", …)`) still resolves to `"notes"`, matching
406
+ * what the kernel reads off the live table object at call time. Falls back
407
+ * to the raw identifier text otherwise.
386
408
  *
387
409
  * @param call - Any call in the chain
410
+ * @param bindings - Scope bindings
388
411
  */
389
- function tableFromStoreChain(call: CallExpression): string | undefined {
412
+ function tableFromStoreChain(
413
+ call: CallExpression,
414
+ bindings: ReadonlyMap<string, InferBinding>,
415
+ ): string | undefined {
390
416
  let current: AstNode | undefined = call;
391
417
  while (current && current.type === "CallExpression") {
392
418
  const c = current as CallExpression;
@@ -394,8 +420,11 @@ function tableFromStoreChain(call: CallExpression): string | undefined {
394
420
  if (!link || link.rootMethod !== "store") break;
395
421
  const leaf = link.methods[link.methods.length - 1]!;
396
422
  if (TABLE_ARG_METHODS.has(leaf)) {
397
- const table = identifierName(c.arguments[0]);
398
- if (table) return table;
423
+ const id = identifierName(c.arguments[0]);
424
+ if (id) {
425
+ const binding = bindings.get(id);
426
+ return binding?.kind === "table" ? binding.ref : id;
427
+ }
399
428
  }
400
429
  const callee = c.callee;
401
430
  if (callee.type === "MemberExpression") {
@@ -344,3 +344,128 @@ export const remove = mounted.remove;
344
344
  expect(manifest.flows?.list?.breaking).toBe(true);
345
345
  });
346
346
  });
347
+
348
+ describe("extractManifest — files-store write methods", () => {
349
+ test("fx.store(files).put(...) infers a write, unannotated", async () => {
350
+ const source = `
351
+ import { on, flow, http, gate, store } from "okengine";
352
+
353
+ export const files = store.files("uploads");
354
+
355
+ export const attach = on(
356
+ http.post("/attach").gate(gate.public),
357
+ flow({
358
+ name: "notes.attach",
359
+ unit: "notes",
360
+ do: async (input, fx) => {
361
+ await fx.store(files).put("key", input.text);
362
+ return { ok: true };
363
+ },
364
+ }),
365
+ );
366
+ `;
367
+ const manifest = await extractFromSources({ "src/flows/attach.ts": source });
368
+
369
+ expect(manifest.flows?.["notes.attach"]?.effects?.writes).toEqual(["files:uploads"]);
370
+ });
371
+
372
+ test("fx.store(files).putImage(...) infers a write, unannotated", async () => {
373
+ const source = `
374
+ import { on, flow, http, gate, store } from "okengine";
375
+
376
+ export const files = store.files("uploads");
377
+
378
+ export const attach = on(
379
+ http.post("/attach").gate(gate.public),
380
+ flow({
381
+ name: "notes.attachImage",
382
+ unit: "notes",
383
+ do: async (input, fx) => {
384
+ await fx.store(files).putImage("key", input.bytes);
385
+ return { ok: true };
386
+ },
387
+ }),
388
+ );
389
+ `;
390
+ const manifest = await extractFromSources({ "src/flows/attach.ts": source });
391
+
392
+ expect(manifest.flows?.["notes.attachImage"]?.effects?.writes).toEqual(["files:uploads"]);
393
+ });
394
+
395
+ test("fx.store(files).list(...) infers a read, unannotated", async () => {
396
+ const source = `
397
+ import { on, flow, every, store } from "okengine";
398
+
399
+ export const files = store.files("uploads");
400
+
401
+ export const sweep = on(
402
+ every("1d"),
403
+ flow({
404
+ name: "notes.sweep",
405
+ unit: "notes",
406
+ do: async (_input, fx) => {
407
+ const keys = await fx.store(files).list();
408
+ return { count: keys.length };
409
+ },
410
+ }),
411
+ );
412
+ `;
413
+ const manifest = await extractFromSources({ "src/flows/sweep.ts": source });
414
+
415
+ expect(manifest.flows?.["notes.sweep"]?.effects?.reads).toEqual(["files:uploads"]);
416
+ });
417
+ });
418
+
419
+ describe("extractManifest — channel medium binder aliasing", () => {
420
+ test("mail.template(...) resolves through `const mail = channel.email(...)`", async () => {
421
+ const source = `
422
+ import { on, flow, http, gate, channel } from "okengine";
423
+
424
+ const mail = channel.email({ from: "Notes <notes@localhost>" });
425
+
426
+ export const noteCreatedMail = mail.template("note-created", {
427
+ locales: ["en", "ar"],
428
+ });
429
+
430
+ export const onCreated = on(
431
+ http.post("/hook").gate(gate.public),
432
+ flow({
433
+ name: "notes.onCreated",
434
+ unit: "notes",
435
+ do: async (payload, fx) => {
436
+ await fx.send(noteCreatedMail, { to: "you@localhost", data: payload });
437
+ },
438
+ }),
439
+ );
440
+ `;
441
+ const manifest = await extractFromSources({ "src/flows/notes.ts": source });
442
+
443
+ expect(manifest.flows?.["notes.onCreated"]?.effects?.sends).toEqual(["note-created"]);
444
+ expect(manifest.channels?.["note-created"]?.medium).toBe("email");
445
+ });
446
+
447
+ test("sms medium binder alias carries its medium (not defaulted to email)", async () => {
448
+ const source = `
449
+ import { on, flow, http, gate, channel } from "okengine";
450
+
451
+ const otp = channel.sms({});
452
+
453
+ export const otpTemplate = otp.template("otp-code", {});
454
+
455
+ export const send = on(
456
+ http.post("/otp").gate(gate.public),
457
+ flow({
458
+ name: "notes.sendOtp",
459
+ unit: "notes",
460
+ do: async (payload, fx) => {
461
+ await fx.send(otpTemplate, { to: "+10000000000", data: payload });
462
+ },
463
+ }),
464
+ );
465
+ `;
466
+ const manifest = await extractFromSources({ "src/flows/otp.ts": source });
467
+
468
+ expect(manifest.flows?.["notes.sendOtp"]?.effects?.sends).toEqual(["otp-code"]);
469
+ expect(manifest.channels?.["otp-code"]?.medium).toBe("sms");
470
+ });
471
+ });
@@ -14,6 +14,7 @@ import type {
14
14
  AiModel,
15
15
  AiPrompt,
16
16
  Channel,
17
+ ChannelMedium,
17
18
  Clock,
18
19
  DeclaredColumn,
19
20
  Effects,
@@ -22,6 +23,7 @@ import type {
22
23
  Journey,
23
24
  Manifest,
24
25
  RateStrategy,
26
+ ResourceRef,
25
27
  SecretContract,
26
28
  Signal,
27
29
  SignalDelivery,
@@ -93,6 +95,8 @@ interface ProjectScope {
93
95
  string,
94
96
  { storeName: string; storeRef: string; unit?: string; breaking?: boolean }
95
97
  >;
98
+ /** Local binding name → medium, for `const x = channel.email(…)` binders. */
99
+ channelMediumBindings: Map<string, ChannelMedium>;
96
100
  }
97
101
 
98
102
  /**
@@ -122,6 +126,7 @@ export async function extractManifest(options: ExtractManifestOptions = {}): Pro
122
126
  flows: {},
123
127
  flowExports: new Map(),
124
128
  resources: new Map(),
129
+ channelMediumBindings: new Map(),
125
130
  };
126
131
 
127
132
  const parsed = files.map((file) => {
@@ -328,7 +333,14 @@ function collectSchemaTable(call: CallExpression, program: AstNode, scope: Proje
328
333
  const entry = { name: tableName, columns };
329
334
  scope.schemaTables.set(tableName, entry);
330
335
  const bindingName = enclosingConstName(call, program);
331
- if (bindingName) scope.schemaTables.set(bindingName, entry);
336
+ if (bindingName) {
337
+ scope.schemaTables.set(bindingName, entry);
338
+ // Declared table name, not the JS binding — resolved by
339
+ // tableFromStoreChain so `const notesTable = store.schema.table("notes",
340
+ // …)` still infers "sql:notes", matching what the kernel reads off the
341
+ // live table object (see ../manifest/sql-resource.ts).
342
+ scope.bindings.set(bindingName, { kind: "table", ref: tableName });
343
+ }
332
344
  }
333
345
 
334
346
  function attachSchemaOption(
@@ -583,6 +595,8 @@ function camelToSnakeKey(key: string): string {
583
595
  .toLowerCase();
584
596
  }
585
597
 
598
+ const CHANNEL_MEDIUM_METHODS = new Set(["email", "sms", "whatsapp", "push"]);
599
+
586
600
  function visitDeclarationCall(call: CallExpression, program: AstNode, scope: ProjectScope): void {
587
601
  const callee = call.callee;
588
602
 
@@ -717,7 +731,17 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
717
731
  }
718
732
  }
719
733
 
720
- if (obj === "channel" && prop === "template") {
734
+ // channel.email(…) / .sms(…) / .whatsapp(…) / .push(…) — medium binder,
735
+ // usually held in a local const and called later as `mail.template(…)`.
736
+ if (obj === "channel" && prop && CHANNEL_MEDIUM_METHODS.has(prop)) {
737
+ const bindingName = enclosingConstName(call, program);
738
+ if (bindingName) {
739
+ scope.channelMediumBindings.set(bindingName, prop as ChannelMedium);
740
+ }
741
+ }
742
+
743
+ const boundMedium = obj ? scope.channelMediumBindings.get(obj) : undefined;
744
+ if ((obj === "channel" && prop === "template") || (prop === "template" && boundMedium)) {
721
745
  const templateName = stringArg(call.arguments[0]);
722
746
  const opts = objectArg(call.arguments[1]);
723
747
  if (templateName) {
@@ -725,7 +749,9 @@ function visitDeclarationCall(call: CallExpression, program: AstNode, scope: Pro
725
749
  const locales = stringArrayProp(opts, "locales");
726
750
  const description = stringProp(opts, "description");
727
751
  scope.channels[templateName] = {
728
- ...(medium ? { medium: medium as Channel["medium"] } : { medium: "email" }),
752
+ ...(medium
753
+ ? { medium: medium as Channel["medium"] }
754
+ : { medium: boundMedium ?? "email" }),
729
755
  ...(locales ? { locales } : {}),
730
756
  ...(description ? { description } : {}),
731
757
  };
@@ -1333,9 +1359,8 @@ function registerResourceMount(
1333
1359
  const resource = baseName ? scope.resources.get(baseName) : undefined;
1334
1360
 
1335
1361
  const unit = resource?.unit;
1336
- const storeRef = resource?.storeRef as Effects["reads"] extends readonly (infer R)[] | undefined
1337
- ? R
1338
- : never;
1362
+ const storeRef: ResourceRef | undefined =
1363
+ resource?.storeRef !== undefined ? (resource.storeRef as ResourceRef) : undefined;
1339
1364
  const effects: Effects | undefined = storeRef
1340
1365
  ? { reads: [storeRef], writes: [storeRef] }
1341
1366
  : undefined;
@@ -155,13 +155,13 @@ export interface DbConfig {
155
155
  /**
156
156
  * Abstract schema declare module (`store.schema.table` exports).
157
157
  * When present, `oke db` emits Drizzle into {@link generated} before sync.
158
- * Default `"src/schema.decl.ts"` (emit skipped if the file is absent).
158
+ * Default `"src/db/schema.decl.ts"` (legacy `"src/schema.decl.ts"` still resolved).
159
159
  */
160
160
  readonly declare?: string;
161
161
  /**
162
162
  * Generated Drizzle schema path written from abstract decls.
163
- * Default `"src/schema.generated.ts"`. Point `drizzle.config.ts` `schema`
164
- * at this file when using the abstract path.
163
+ * Default `"src/db/schema.generated.ts"` (legacy `"src/schema.generated.ts"`).
164
+ * Point `drizzle.config.ts` `schema` at this file when using the abstract path.
165
165
  */
166
166
  readonly generated?: string;
167
167
  /**
@@ -58,7 +58,9 @@ export function mintClaimCode(now: () => number = Date.now): ClaimCodeState {
58
58
  */
59
59
  export function printClaimCodeOnce(
60
60
  state: ClaimCodeState,
61
- write: (line: string) => void = (line) => console.log(line),
61
+ write: (line: string) => void = (line) => {
62
+ process.stdout.write(line);
63
+ },
62
64
  ): void {
63
65
  if (state.printed) return;
64
66
  state.printed = true;