okengine 0.13.0 → 0.15.2

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 (277) hide show
  1. package/manifest.v1.schema.json +7 -1
  2. package/package.json +4 -5
  3. package/site/content/docs/ai/mcp.mdx +27 -2
  4. package/site/content/docs/elements/ai.mdx +59 -11
  5. package/site/content/docs/elements/channel.mdx +1 -0
  6. package/site/content/docs/elements/clock.mdx +30 -12
  7. package/site/content/docs/elements/flow.mdx +9 -3
  8. package/site/content/docs/elements/gate.mdx +25 -22
  9. package/site/content/docs/elements/signal.mdx +71 -25
  10. package/site/content/docs/elements/store.mdx +87 -28
  11. package/site/content/docs/elements/vault.mdx +1 -0
  12. package/site/content/docs/get-started/basic-usage.mdx +22 -13
  13. package/site/content/docs/get-started/index.mdx +5 -0
  14. package/site/content/docs/get-started/installation.mdx +2 -2
  15. package/site/content/docs/get-started/introduction.mdx +11 -3
  16. package/site/content/docs/get-started/meta.json +1 -1
  17. package/site/content/docs/get-started/project-structure.mdx +932 -0
  18. package/site/content/docs/reference/cli.md +13 -10
  19. package/site/content/docs/reference/client.mdx +25 -21
  20. package/site/content/docs/reference/fx.mdx +27 -22
  21. package/site/content/docs/reference/security.md +1 -1
  22. package/src/auth/bindings.ts +14 -5
  23. package/src/auth/cross-plane.ts +10 -3
  24. package/src/cli/ai-setup/ai-setup.test.ts +62 -0
  25. package/src/cli/ai-setup/apply.ts +32 -1
  26. package/src/cli/ai-setup/recommend.test.ts +25 -0
  27. package/src/cli/ai-setup/recommend.ts +8 -3
  28. package/src/cli/attach-host-console.test.ts +3 -2
  29. package/src/cli/attach-host-console.ts +3 -0
  30. package/src/cli/build.test.ts +4 -2
  31. package/src/cli/db-seed.ts +48 -15
  32. package/src/cli/db.test.ts +104 -0
  33. package/src/cli/db.ts +122 -10
  34. package/src/cli/dev-controls.test.ts +20 -0
  35. package/src/cli/dev-controls.ts +13 -0
  36. package/src/cli/dev.test.ts +15 -4
  37. package/src/cli/dev.ts +102 -29
  38. package/src/cli/ensure-drizzle-config.ts +2 -1
  39. package/src/cli/tui/DevLive.tsx +12 -2
  40. package/src/cli/tui/keys.test.ts +2 -1
  41. package/src/cli/tui/keys.ts +3 -2
  42. package/src/compiler/aot.test.ts +1 -1
  43. package/src/compiler/effects-infer.ts +58 -3
  44. package/src/compiler/extract.test.ts +458 -29
  45. package/src/compiler/extract.ts +387 -69
  46. package/src/compiler/flow-path.test.ts +96 -0
  47. package/src/compiler/flow-path.ts +173 -0
  48. package/src/compiler/generate-adopt.test.ts +112 -3
  49. package/src/compiler/generate-adopt.ts +280 -26
  50. package/src/compiler/response.ts +45 -1
  51. package/src/compiler/schema-from-ast.ts +609 -0
  52. package/src/console/server/ai.ts +5 -2
  53. package/src/console/server/bind.ts +2 -2
  54. package/src/console/server/console.test.ts +3 -0
  55. package/src/console/server/flows.ts +121 -40
  56. package/src/console/server/invoke-user-flow.test.ts +3 -1
  57. package/src/console/server/invoke-user-flow.ts +17 -2
  58. package/src/console/server/iso-at.test.ts +9 -0
  59. package/src/console/server/iso-at.ts +11 -0
  60. package/src/console/server/security-headers.ts +6 -3
  61. package/src/console/server/serve.ts +17 -2
  62. package/src/console/server/spa-proxy.test.ts +1 -0
  63. package/src/console/server/sql-catalog.ts +147 -7
  64. package/src/console/server/sql-rls.test.ts +16 -0
  65. package/src/console/server/state.ts +8 -0
  66. package/src/console/server/store.test.ts +7 -4
  67. package/src/console/server/store.ts +142 -58
  68. package/src/console/ui-next/dist/assets/agent-disclosure-C0X1fbWF.js +1 -0
  69. package/src/console/ui-next/dist/assets/cache-glyph-F1FI122b.js +1 -0
  70. package/src/console/ui-next/dist/assets/{call-pii-button-bqkxMrJH.js → call-pii-button-DEDCl_j3.js} +1 -1
  71. package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +1 -0
  72. package/src/console/ui-next/dist/assets/{confirm-sheet-DDCRmG62.js → confirm-sheet-3ptDzXbz.js} +1 -1
  73. package/src/console/ui-next/dist/assets/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
  74. package/src/console/ui-next/dist/assets/{explorer-empty-HjTnVQoR.js → explorer-empty-BvYrygyO.js} +1 -1
  75. package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +1 -0
  76. package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.js → highlighted-json-CS_O8L-r.js} +1 -1
  77. package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +1 -0
  78. package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +66 -0
  79. package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +2 -0
  80. package/src/console/ui-next/dist/assets/{link-DF7SZ9Ek.js → link-COeyggt-.js} +1 -1
  81. package/src/console/ui-next/dist/assets/observability-page-HK9-BO8z.js +4 -0
  82. package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -0
  83. package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +18 -0
  84. package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +1 -0
  85. package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +1 -0
  86. package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +1 -0
  87. package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +41 -0
  88. package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +2 -0
  89. package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +54 -0
  90. package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +1 -0
  91. package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.js → use-vault-list-BLbZhzyF.js} +1 -1
  92. package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +2 -0
  93. package/src/console/ui-next/dist/index.html +6 -6
  94. package/src/console/ui-next/seed-invoke-host.ts +2 -0
  95. package/src/console/ui-next/src/client.ts +35 -7
  96. package/src/console/ui-next/src/components/motion/table/index.tsx +16 -0
  97. package/src/console/ui-next/src/components/motion/table/types.ts +4 -0
  98. package/src/console/ui-next/src/components/ui/kbd.tsx +2 -2
  99. package/src/console/ui-next/src/features/flows/graph/build-flow-graph.test.ts +18 -0
  100. package/src/console/ui-next/src/features/flows/graph/build-flow-graph.ts +14 -1
  101. package/src/console/ui-next/src/features/flows/graph/neighborhood.test.ts +17 -0
  102. package/src/console/ui-next/src/features/flows/graph/neighborhood.ts +16 -3
  103. package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +3 -1
  104. package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
  105. package/src/console/ui-next/src/features/flows/traces/http-method.ts +19 -0
  106. package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +9 -3
  107. package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +29 -2
  108. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
  109. package/src/console/ui-next/src/features/observability/lib/ask-count.test.ts +25 -0
  110. package/src/console/ui-next/src/features/observability/lib/ask-count.ts +4 -1
  111. package/src/console/ui-next/src/features/store/detail/browse-section.tsx +45 -1
  112. package/src/console/ui-next/src/features/store/detail/resource-panel.tsx +78 -16
  113. package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +155 -14
  114. package/src/console/ui-next/src/features/store/explorer/store-tree.tsx +33 -133
  115. package/src/console/ui-next/src/features/store/files/file-preview.tsx +4 -1
  116. package/src/console/ui-next/src/features/store/grid/rls-policy-sheet.tsx +326 -165
  117. package/src/console/ui-next/src/features/store/grid/sql-insert-sheet.tsx +6 -0
  118. package/src/console/ui-next/src/features/store/grid/store-data-grid.tsx +50 -3
  119. package/src/console/ui-next/src/features/store/lib/files-meta.test.ts +3 -0
  120. package/src/console/ui-next/src/features/store/lib/files-meta.ts +12 -0
  121. package/src/console/ui-next/src/features/store/lib/query-defaults.test.ts +25 -0
  122. package/src/console/ui-next/src/features/store/lib/query-defaults.ts +85 -3
  123. package/src/console/ui-next/src/features/store/lib/query-gate.ts +30 -4
  124. package/src/console/ui-next/src/features/store/lib/rls-policy.test.ts +139 -0
  125. package/src/console/ui-next/src/features/store/lib/rls-policy.ts +223 -21
  126. package/src/console/ui-next/src/features/store/lib/schema-graph.ts +4 -0
  127. package/src/console/ui-next/src/features/store/lib/sql-catalog.test.ts +24 -0
  128. package/src/console/ui-next/src/features/store/lib/sql-catalog.ts +19 -0
  129. package/src/console/ui-next/src/features/store/lib/store-tree.test.ts +6 -0
  130. package/src/console/ui-next/src/features/store/lib/store-tree.ts +13 -0
  131. package/src/console/ui-next/src/features/store/query/query-console.tsx +48 -14
  132. package/src/console/ui-next/src/features/store/query/query-gate-menu.tsx +19 -20
  133. package/src/console/ui-next/src/features/store/query/query-gate-parts.tsx +2 -1
  134. package/src/console/ui-next/src/features/store/query/query-results.tsx +110 -83
  135. package/src/console/ui-next/src/features/store/schema/schema-table-node.tsx +17 -0
  136. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +112 -72
  137. package/src/console/ui-next/src/features/units/detail/effects-summary.tsx +12 -4
  138. package/src/console/ui-next/src/features/units/lib/contract-input.test.ts +206 -1
  139. package/src/console/ui-next/src/features/units/lib/contract-input.ts +55 -0
  140. package/src/console/ui-next/src/features/vault/lib/types.ts +1 -1
  141. package/src/docker/compose-health.test.ts +27 -0
  142. package/src/docker/compose-health.ts +19 -3
  143. package/src/docker/docker.test.ts +4 -1
  144. package/src/docker/dockerfile.ts +1 -1
  145. package/src/docker/recipes/pgdog.ts +2 -0
  146. package/src/drivers/ai-anthropic.ts +5 -0
  147. package/src/drivers/ai-ollama.ts +49 -30
  148. package/src/drivers/ai-openai-compatible.ts +57 -46
  149. package/src/drivers/ai-providers.test.ts +3 -0
  150. package/src/drivers/bun-native-completeness.test.ts +7 -9
  151. package/src/drivers/clock-postgres.ts +13 -9
  152. package/src/drivers/instances-postgres.ts +4 -6
  153. package/src/drivers/journal-postgres.ts +13 -9
  154. package/src/drivers/pg-rls.test.ts +87 -0
  155. package/src/drivers/pg-rls.ts +233 -7
  156. package/src/drivers/pglite.ts +26 -3
  157. package/src/drivers/postgres.test.ts +23 -0
  158. package/src/drivers/postgres.ts +133 -9
  159. package/src/drivers/redis.ts +11 -4
  160. package/src/drivers/signal-redis.ts +24 -14
  161. package/src/drivers/signal-types.ts +2 -1
  162. package/src/drivers/types.ts +10 -0
  163. package/src/elements/ai/declare.ts +109 -0
  164. package/src/elements/ai/errors.test.ts +5 -1
  165. package/src/elements/ai/errors.ts +30 -2
  166. package/src/elements/ai/eval.ts +4 -6
  167. package/src/elements/ai/mcp-client.test.ts +206 -0
  168. package/src/elements/ai/mcp-client.ts +362 -0
  169. package/src/elements/ai/mcp-http.ts +159 -0
  170. package/src/elements/ai/mcp-mock.ts +134 -0
  171. package/src/elements/ai/mcp-protocol.ts +234 -0
  172. package/src/elements/ai/mcp-stdio.test.ts +50 -0
  173. package/src/elements/ai/mcp-stdio.ts +212 -0
  174. package/src/elements/ai/mcp-transport.ts +70 -0
  175. package/src/elements/ai/runtime.ts +159 -29
  176. package/src/elements/ai.test.ts +139 -0
  177. package/src/elements/ai.ts +16 -0
  178. package/src/elements/clock/declare.ts +26 -1
  179. package/src/elements/clock/health.test.ts +43 -0
  180. package/src/elements/clock/runtime.ts +55 -2
  181. package/src/elements/clock/schedule.ts +71 -142
  182. package/src/elements/clock.test.ts +9 -40
  183. package/src/elements/clock.ts +6 -1
  184. package/src/elements/gate/boot.ts +5 -5
  185. package/src/elements/gate/declare.ts +36 -4
  186. package/src/elements/gate/flatten.ts +1 -1
  187. package/src/elements/gate/runtime.ts +1 -1
  188. package/src/elements/index.ts +2 -0
  189. package/src/elements/signal/declare.ts +2 -1
  190. package/src/elements/signal/runtime.ts +16 -1
  191. package/src/elements/signal.ts +1 -0
  192. package/src/elements/store/cache.test.ts +2 -0
  193. package/src/elements/store/cache.ts +3 -3
  194. package/src/elements/store/declare.ts +9 -6
  195. package/src/elements/store/emit-drizzle.ts +44 -8
  196. package/src/elements/store/resource.test.ts +1 -1
  197. package/src/elements/store/rls-identity.test.ts +95 -0
  198. package/src/elements/store/rls-identity.ts +116 -0
  199. package/src/elements/store/runtime.ts +4 -0
  200. package/src/elements/store/schema-decl.test.ts +37 -0
  201. package/src/elements/store/schema-decl.ts +161 -0
  202. package/src/elements/store/seed.ts +1 -1
  203. package/src/elements/store/sql-condition.test.ts +12 -0
  204. package/src/elements/store/sql-condition.ts +26 -2
  205. package/src/elements/store/sql-rls-isolation.test.ts +165 -0
  206. package/src/elements/store/sql-rls-stamp.test.ts +117 -0
  207. package/src/elements/store/sql-session.ts +81 -3
  208. package/src/elements/store/upsert-app.test.ts +1 -1
  209. package/src/elements/store.ts +14 -0
  210. package/src/elements/vault/builtin-adapter.ts +17 -0
  211. package/src/full.ts +2 -0
  212. package/src/http.ts +20 -3
  213. package/src/index.ts +14 -1
  214. package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
  215. package/src/kernel/adopt-routes.ts +30 -2
  216. package/src/kernel/app.ts +227 -77
  217. package/src/kernel/auto-cache.test.ts +8 -8
  218. package/src/kernel/auto-registry.test.ts +54 -9
  219. package/src/kernel/boot-bind/ai.ts +24 -0
  220. package/src/kernel/boot-bind/clock.ts +2 -0
  221. package/src/kernel/boot.test.ts +4 -4
  222. package/src/kernel/boot.ts +5 -3
  223. package/src/kernel/effects-stamping.test.ts +3 -3
  224. package/src/kernel/element-registries.ts +15 -5
  225. package/src/kernel/errors.ts +24 -0
  226. package/src/kernel/flow-units.ts +64 -0
  227. package/src/kernel/flow.ts +29 -17
  228. package/src/kernel/fx-dead-letters.test.ts +77 -0
  229. package/src/kernel/fx.test.ts +22 -0
  230. package/src/kernel/fx.ts +135 -6
  231. package/src/kernel/horizontal-child.ts +4 -4
  232. package/src/kernel/http-path-pending.ts +20 -0
  233. package/src/kernel/http-resource.ts +4 -0
  234. package/src/kernel/http-stream.test.ts +174 -0
  235. package/src/kernel/index.ts +2 -0
  236. package/src/kernel/pipeline.test.ts +2 -2
  237. package/src/kernel/plugin-elements.test.ts +1 -1
  238. package/src/kernel/plugin-needs.test.ts +1 -1
  239. package/src/kernel/ready.test.ts +2 -2
  240. package/src/kernel/resource-mount.test.ts +14 -0
  241. package/src/kernel/run-duration.test.ts +2 -2
  242. package/src/kernel/stamp-http.test.ts +79 -0
  243. package/src/kernel/stamp-http.ts +75 -0
  244. package/src/kernel/triggers.ts +52 -27
  245. package/src/manifest/mcp-ref.ts +88 -0
  246. package/src/manifest/types.ts +41 -2
  247. package/src/manifest/validate.test.ts +20 -0
  248. package/src/mcp/docs-server.ts +1 -1
  249. package/src/mcp/server.ts +1 -1
  250. package/src/plugins/compression.test.ts +21 -0
  251. package/src/plugins/compression.ts +1 -0
  252. package/src/release/measure.ts +2 -2
  253. package/src/runtime/bun.ts +41 -4
  254. package/src/runtime/json-code-block.test.ts +2 -2
  255. package/src/runtime/serve.test.ts +4 -4
  256. package/src/term.test.ts +52 -4
  257. package/src/term.ts +125 -17
  258. package/src/test/reset-element-registries.ts +10 -3
  259. package/src/cli/ask-seed.test.ts +0 -96
  260. package/src/cli/ask-seed.ts +0 -82
  261. package/src/console/ui-next/dist/assets/cache-glyph-CLPBqZeb.js +0 -1
  262. package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
  263. package/src/console/ui-next/dist/assets/flows-page-Dg8CTE29.js +0 -1
  264. package/src/console/ui-next/dist/assets/http-method-BJ92Z_ke.js +0 -1
  265. package/src/console/ui-next/dist/assets/index-Bp-R7jtM.js +0 -66
  266. package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
  267. package/src/console/ui-next/dist/assets/observability-page-DAnpEaq1.js +0 -4
  268. package/src/console/ui-next/dist/assets/query-gate-parts-1m8m1iNp.js +0 -2
  269. package/src/console/ui-next/dist/assets/react-B1ML8gxg.js +0 -1
  270. package/src/console/ui-next/dist/assets/replica-lag-C8_BRt2x.js +0 -18
  271. package/src/console/ui-next/dist/assets/sheet-form-D-ata7jy.js +0 -1
  272. package/src/console/ui-next/dist/assets/shortcut-keys-DO4IsVqv.js +0 -1
  273. package/src/console/ui-next/dist/assets/skeleton-CL_X0GCj.js +0 -1
  274. package/src/console/ui-next/dist/assets/store-page-v3LXdYpr.js +0 -45
  275. package/src/console/ui-next/dist/assets/trace-detail-sheet-D16lWQMt.js +0 -2
  276. package/src/console/ui-next/dist/assets/units-page-BtQ0bqMe.js +0 -1
  277. package/src/console/ui-next/dist/assets/vault-page-Ca-MvcmJ.js +0 -2
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * `oke db seed` — load `defineSeed`, boot the app store, run essential +
3
- * env-selected category (`dev` | `prod`).
3
+ * env-selected category (`dev` | `prod`). Tallies SQL upserts and `kv.set`.
4
4
  */
5
5
 
6
6
  import { resolve } from "node:path";
7
- import { isCancel, text } from "@clack/prompts";
7
+ import { intro, isCancel, outro, text } from "@clack/prompts";
8
8
  import type { ConfigEnv, OkeConfig } from "../config/index.ts";
9
9
  import {
10
10
  normalizeSeedFns,
@@ -25,11 +25,13 @@ import { EXIT_OK, EXIT_RUNTIME, EXIT_USAGE } from "./exit.ts";
25
25
  import { loadOkeConfig } from "./load-config.ts";
26
26
  import { resolveDevSqlEnv } from "./resolve-dev-sql-env.ts";
27
27
 
28
- /** Per-function upsert outcome counts. */
28
+ /** Per-function upsert / KV-set outcome counts. */
29
29
  export interface SeedTally {
30
30
  upserted: number;
31
31
  changed: number;
32
32
  alreadyExisted: number;
33
+ /** `fx.store(kv).set` calls (cache or durable). */
34
+ keys: number;
33
35
  }
34
36
 
35
37
  /** Options for {@link runSeed}. */
@@ -56,6 +58,11 @@ export interface SeedOptions {
56
58
  */
57
59
  readonly confirmEnv?: (env: ConfigEnv, target: string) => Promise<boolean>;
58
60
  readonly stdinIsTTY?: boolean;
61
+ /**
62
+ * Clack `intro` / `outro` for the standalone `oke db seed` path.
63
+ * Default: on when stdin is a TTY.
64
+ */
65
+ readonly intro?: boolean;
59
66
  }
60
67
 
61
68
  /**
@@ -127,12 +134,13 @@ export async function runSeedFns(
127
134
  ): Promise<void> {
128
135
  for (let i = 0; i < fns.length; i++) {
129
136
  const fn = fns[i]!;
130
- const tally: SeedTally = { upserted: 0, changed: 0, alreadyExisted: 0 };
137
+ const tally: SeedTally = { upserted: 0, changed: 0, alreadyExisted: 0, keys: 0 };
131
138
  const instrumented = instrumentFxUpserts(fx, tally);
132
139
  await fn(instrumented);
133
140
  const label = fn.name.length > 0 ? fn.name : String(i);
141
+ const kv = tally.keys > 0 ? ` · keys ${tally.keys}` : "";
134
142
  write(
135
- `oke db seed: ${category} ${label} — upserted ${tally.upserted} · changed ${tally.changed} · already-existed ${tally.alreadyExisted}\n`,
143
+ `oke db seed: ${category} ${label} — upserted ${tally.upserted} · changed ${tally.changed} · already-existed ${tally.alreadyExisted}${kv}\n`,
136
144
  );
137
145
  }
138
146
  }
@@ -233,7 +241,13 @@ export async function bootSeedFx(
233
241
  if (!app) {
234
242
  throw new Error(`oke db seed: no oke() app export in ${entryAbs}`);
235
243
  }
236
- await app.boot({ env, startScheduler: false, config: config ?? undefined, rootDir: cwd });
244
+ await app.boot({
245
+ env,
246
+ startScheduler: false,
247
+ config: config ?? undefined,
248
+ rootDir: cwd,
249
+ docker: process.env.OKE_DOCKER === "1",
250
+ });
237
251
  const storeRuntime = app.elements?.store ?? app.bootResult?.store;
238
252
  if (!storeRuntime) {
239
253
  throw new Error(
@@ -243,6 +257,7 @@ export async function bootSeedFx(
243
257
  const fx = createFx({
244
258
  flow: "oke.db.seed",
245
259
  storeRuntime,
260
+ rlsBypass: true,
246
261
  });
247
262
  return {
248
263
  fx,
@@ -262,6 +277,14 @@ export async function runSeed(options: SeedOptions = {}): Promise<number> {
262
277
  const cwd = options.cwd ?? process.cwd();
263
278
  const loaded = await loadOkeConfig(cwd).catch(() => null);
264
279
  const env = options.env ?? (await resolveDevSqlEnv(cwd));
280
+ const tty = options.stdinIsTTY ?? Boolean(process.stdin.isTTY);
281
+ const useIntro = options.intro !== false && tty;
282
+ if (useIntro) intro("oke db seed");
283
+
284
+ const finish = (code: number, message: string): number => {
285
+ if (useIntro) outro(message);
286
+ return code;
287
+ };
265
288
 
266
289
  let targetLabel = String(env);
267
290
  try {
@@ -274,7 +297,6 @@ export async function runSeed(options: SeedOptions = {}): Promise<number> {
274
297
 
275
298
  if (env === "dev" || env === "prod") {
276
299
  if (options.force !== true) {
277
- const tty = options.stdinIsTTY ?? Boolean(process.stdin.isTTY);
278
300
  const confirm =
279
301
  options.confirmEnv ??
280
302
  (tty
@@ -288,7 +310,7 @@ export async function runSeed(options: SeedOptions = {}): Promise<number> {
288
310
  const ok = await confirm(env, targetLabel);
289
311
  if (!ok) {
290
312
  write("oke db seed: cancelled\n");
291
- return EXIT_USAGE;
313
+ return finish(EXIT_USAGE, "Cancelled.");
292
314
  }
293
315
  }
294
316
  }
@@ -308,10 +330,10 @@ export async function runSeed(options: SeedOptions = {}): Promise<number> {
308
330
  stop = session.stop;
309
331
  await executeSeedDef(def, env, session.fx, write);
310
332
  write("oke db seed: ok\n");
311
- return EXIT_OK;
333
+ return finish(EXIT_OK, "oke db seed: ok");
312
334
  } catch (err) {
313
335
  write(`oke db seed: ${err instanceof Error ? err.message : String(err)}\n`);
314
- return EXIT_RUNTIME;
336
+ return finish(EXIT_RUNTIME, "oke db seed: failed");
315
337
  } finally {
316
338
  if (stop) await stop().catch(() => {});
317
339
  }
@@ -374,14 +396,18 @@ function instrumentFxUpserts(fx: Fx, tally: SeedTally): Fx {
374
396
 
375
397
  function instrumentUpsertHandle<T>(handle: T, tally: SeedTally): T {
376
398
  if (!handle || typeof handle !== "object") return handle;
377
- const h = handle as { upsert?: (...args: unknown[]) => Promise<{ status: UpsertStatus }> };
378
- if (typeof h.upsert !== "function") return handle;
379
- const orig = h.upsert.bind(h);
399
+ const h = handle as {
400
+ upsert?: (...args: unknown[]) => Promise<{ status: UpsertStatus }>;
401
+ set?: (...args: unknown[]) => Promise<unknown>;
402
+ };
403
+ const origUpsert = typeof h.upsert === "function" ? h.upsert.bind(h) : undefined;
404
+ const origSet = typeof h.set === "function" ? h.set.bind(h) : undefined;
405
+ if (!origUpsert && !origSet) return handle;
380
406
  return new Proxy(handle as object, {
381
407
  get(target, prop, receiver) {
382
- if (prop === "upsert") {
408
+ if (prop === "upsert" && origUpsert) {
383
409
  return async (...args: unknown[]) => {
384
- const result = await orig(...args);
410
+ const result = await origUpsert(...args);
385
411
  const status =
386
412
  result && typeof result === "object" && "status" in result
387
413
  ? (result as { status: UpsertStatus }).status
@@ -392,6 +418,13 @@ function instrumentUpsertHandle<T>(handle: T, tally: SeedTally): T {
392
418
  return result;
393
419
  };
394
420
  }
421
+ if (prop === "set" && origSet) {
422
+ return async (...args: unknown[]) => {
423
+ const result = await origSet(...args);
424
+ tally.keys += 1;
425
+ return result;
426
+ };
427
+ }
395
428
  return Reflect.get(target, prop, receiver);
396
429
  },
397
430
  }) as T;
@@ -11,9 +11,11 @@ import { createFx } from "../kernel/fx.ts";
11
11
  import { defineSeed, type SeedFn } from "../elements/store/seed.ts";
12
12
  import { EXIT_OK, EXIT_RUNTIME, EXIT_USAGE } from "./exit.ts";
13
13
  import {
14
+ createHintsForRenameOrCreate,
14
15
  dbCli,
15
16
  emitAbstractSchemaPrestep,
16
17
  executeSeedDef,
18
+ formatKitError,
17
19
  runSeedFns,
18
20
  runDb,
19
21
  runGenerate,
@@ -116,6 +118,13 @@ describe("oke db", () => {
116
118
  expect(out.join("")).toContain("0001_init.sql");
117
119
  });
118
120
 
121
+ test("formatKitError joins code message and sql", () => {
122
+ expect(formatKitError({ code: "query_error", message: "boom", sql: "CREATE TABLE t" })).toBe(
123
+ " — query_error — boom — CREATE TABLE t",
124
+ );
125
+ expect(formatKitError(undefined)).toBe("");
126
+ });
127
+
119
128
  test("missing_hints exits runtime", async () => {
120
129
  const out: string[] = [];
121
130
  const code = await runDb("push", {
@@ -131,6 +140,75 @@ describe("oke db", () => {
131
140
  expect(out.join("")).toContain("DROP TABLE leftover");
132
141
  });
133
142
 
143
+ test("createHintsForRenameOrCreate maps rename_or_create only", () => {
144
+ expect(
145
+ createHintsForRenameOrCreate([
146
+ { type: "rename_or_create", kind: "table", entity: ["public", "activity"] },
147
+ {
148
+ type: "confirm_data_loss",
149
+ kind: "table",
150
+ entity: ["public", "old"],
151
+ reason: "non_empty",
152
+ },
153
+ ]),
154
+ ).toEqual([{ type: "create", kind: "table", entity: ["public", "activity"] }]);
155
+ });
156
+
157
+ test("push retries rename_or_create as create", async () => {
158
+ const hints: unknown[] = [];
159
+ const out: string[] = [];
160
+ const code = await runPush("/tmp/drizzle.config.ts", (t) => out.push(t), {
161
+ skipEmit: true,
162
+ pushFn: async (opts) => {
163
+ hints.push(opts.hints);
164
+ if (!opts.hints) {
165
+ return {
166
+ status: "missing_hints",
167
+ unresolved: [{ type: "rename_or_create", kind: "table", entity: ["public", "notes"] }],
168
+ };
169
+ }
170
+ return { status: "ok" };
171
+ },
172
+ });
173
+ expect(code).toBe(EXIT_OK);
174
+ expect(hints[1]).toEqual([{ type: "create", kind: "table", entity: ["public", "notes"] }]);
175
+ expect(out.join("")).toContain("creating 1 new object");
176
+ expect(out.join("")).toContain("oke db push: ok");
177
+ });
178
+
179
+ test("push does not retry confirm_data_loss-only missing_hints", async () => {
180
+ let calls = 0;
181
+ const code = await runPush("/tmp/drizzle.config.ts", () => {}, {
182
+ skipEmit: true,
183
+ pushFn: async () => {
184
+ calls += 1;
185
+ return {
186
+ status: "missing_hints",
187
+ unresolved: [{ type: "confirm_data_loss", kind: "table", entity: ["public", "notes"] }],
188
+ };
189
+ },
190
+ });
191
+ expect(code).toBe(EXIT_RUNTIME);
192
+ expect(calls).toBe(1);
193
+ });
194
+
195
+ test("explain does not auto-create on missing_hints", async () => {
196
+ let calls = 0;
197
+ const code = await runPush("/tmp/drizzle.config.ts", () => {}, {
198
+ skipEmit: true,
199
+ explain: true,
200
+ pushFn: async () => {
201
+ calls += 1;
202
+ return {
203
+ status: "missing_hints",
204
+ unresolved: [{ type: "rename_or_create", kind: "table", entity: ["public", "notes"] }],
205
+ };
206
+ },
207
+ });
208
+ expect(code).toBe(EXIT_RUNTIME);
209
+ expect(calls).toBe(1);
210
+ });
211
+
134
212
  test("resolveDrizzleConfigPath uses db.config from oke.config", async () => {
135
213
  const dir = await mkdtemp(join(tmpdir(), "oke-db-"));
136
214
  await writeFile(
@@ -551,6 +629,32 @@ describe("oke db seed", () => {
551
629
  expect(out.join("")).toContain("dev volume — upserted 0 · changed 1 · already-existed 0");
552
630
  });
553
631
 
632
+ test("runSeedFns tallies kv.set as keys", async () => {
633
+ const out: string[] = [];
634
+ const fx = {
635
+ store: () => ({
636
+ set: async () => undefined,
637
+ }),
638
+ } as unknown as ReturnType<typeof createFx>;
639
+ await runSeedFns(
640
+ "dev",
641
+ [
642
+ async function volume(seedFx) {
643
+ const kv = seedFx.store("kv" as never) as unknown as {
644
+ set: (key: string, value: unknown) => Promise<void>;
645
+ };
646
+ await kv.set("ENG-12", { title: "SSO" });
647
+ await kv.set("ENG-14", { title: "Polish" });
648
+ },
649
+ ],
650
+ fx,
651
+ (t) => out.push(t),
652
+ );
653
+ expect(out.join("")).toContain(
654
+ "dev volume — upserted 0 · changed 0 · already-existed 0 · keys 2",
655
+ );
656
+ });
657
+
554
658
  test("executeSeedDef is the category source of truth", async () => {
555
659
  const order: string[] = [];
556
660
  const out: string[] = [];
package/src/cli/db.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import { resolve } from "node:path";
11
+ import type { Hint as DrizzleKitHint } from "drizzle-kit/cli";
11
12
  import {
12
13
  dialectFromDriverId,
13
14
  maybeEmitDomainSchema,
@@ -17,6 +18,7 @@ import { loadPluginTablesFromAppEntry } from "../elements/store/load-plugin-tabl
17
18
  import { resolveDriverId, type ConfigEnv, type OkeConfig } from "../config/index.ts";
18
19
  import type { TableContribution } from "../kernel/plugin.ts";
19
20
  import { runSeed, type SeedOptions } from "./db-seed.ts";
21
+ import { installOkeRlsHelpers as runOkeRlsHelperStatements } from "../drivers/pg-rls.ts";
20
22
  import { resolveDrizzleKitEnv } from "./drizzle-env.ts";
21
23
  import { resolveDevSqlEnv } from "./resolve-dev-sql-env.ts";
22
24
  import { EXIT_OK, EXIT_RUNTIME, EXIT_USAGE } from "./exit.ts";
@@ -90,7 +92,10 @@ export interface DbOptions {
90
92
  readonly config?: string;
91
93
  readonly write?: (text: string) => void;
92
94
  /** Injectable push (tests). */
93
- readonly pushFn?: (opts: { config: string }) => Promise<DbKitResult>;
95
+ readonly pushFn?: (opts: {
96
+ config: string;
97
+ hints?: readonly DbKitHint[];
98
+ }) => Promise<DbKitResult>;
94
99
  /** Injectable generate (tests). */
95
100
  readonly generateFn?: (opts: {
96
101
  config: string;
@@ -129,13 +134,61 @@ export interface DbOptions {
129
134
  readonly stdinIsTTY?: boolean;
130
135
  }
131
136
 
137
+ /** drizzle-kit SDK hint — create vs rename vs confirm data loss. */
138
+ export type DbKitHint =
139
+ | {
140
+ readonly type: "create";
141
+ readonly kind: string;
142
+ readonly entity: readonly string[];
143
+ }
144
+ | {
145
+ readonly type: "rename";
146
+ readonly kind: string;
147
+ readonly from: readonly string[];
148
+ readonly to: readonly string[];
149
+ }
150
+ | {
151
+ readonly type: "confirm_data_loss";
152
+ readonly kind: string;
153
+ readonly entity: readonly string[];
154
+ };
155
+
132
156
  /** Minimal drizzle-kit envelope we handle. */
133
157
  export interface DbKitResult {
134
158
  readonly status: "ok" | "no_changes" | "missing_hints" | "error" | string;
135
- readonly error?: { readonly code?: string; readonly message?: string };
159
+ readonly error?: {
160
+ readonly code?: string;
161
+ readonly message?: string;
162
+ readonly sql?: string;
163
+ };
136
164
  readonly unresolved?: readonly unknown[];
137
165
  }
138
166
 
167
+ /**
168
+ * Auto-resolve drizzle-kit `rename_or_create` as create (dev push).
169
+ * Leaves `confirm_data_loss` for the caller to fail on.
170
+ *
171
+ * @param unresolved - Kit `missing_hints` payload
172
+ */
173
+ export function createHintsForRenameOrCreate(
174
+ unresolved: readonly unknown[] | undefined,
175
+ ): DbKitHint[] {
176
+ const hints: DbKitHint[] = [];
177
+ if (!unresolved) return hints;
178
+ for (const item of unresolved) {
179
+ if (!item || typeof item !== "object") continue;
180
+ const rec = item as Record<string, unknown>;
181
+ if (rec.type !== "rename_or_create") continue;
182
+ if (typeof rec.kind !== "string" || !Array.isArray(rec.entity)) continue;
183
+ hints.push({
184
+ type: "create",
185
+ kind: rec.kind,
186
+ entity: rec.entity.map((part) => String(part)),
187
+ });
188
+ }
189
+ return hints;
190
+ }
191
+
139
192
  /**
140
193
  * Resolve the drizzle config path for a project.
141
194
  *
@@ -295,19 +348,30 @@ export async function runPush(
295
348
 
296
349
  const pushFn =
297
350
  options.pushFn ??
298
- (async (opts: { config: string }) => {
351
+ (async (opts: { config: string; hints?: readonly DbKitHint[] }) => {
299
352
  const { push } = await import("drizzle-kit/cli");
300
353
  return (await push({
301
354
  config: opts.config,
302
355
  ...(options.explain ? { explain: true } : {}),
356
+ ...(opts.hints && opts.hints.length > 0
357
+ ? { hints: [...opts.hints] as DrizzleKitHint[] }
358
+ : {}),
303
359
  })) as DbKitResult;
304
360
  });
305
361
 
306
362
  let result: DbKitResult;
307
363
  try {
308
- result = await withDrizzleKitEnv(cwd, loaded?.config, env, () =>
309
- pushFn({ config: configPath }),
310
- );
364
+ result = await withDrizzleKitEnv(cwd, loaded?.config, env, async () => {
365
+ await installOkeRlsHelpers(write);
366
+ const first = await pushFn({ config: configPath });
367
+ if (options.explain || first.status !== "missing_hints") return first;
368
+ const createHints = createHintsForRenameOrCreate(first.unresolved);
369
+ if (createHints.length === 0) return first;
370
+ write(
371
+ `oke db push: creating ${createHints.length} new ${createHints.length === 1 ? "object" : "objects"}\n`,
372
+ );
373
+ return pushFn({ config: configPath, hints: createHints });
374
+ });
311
375
  } catch (err) {
312
376
  write(`oke db push: ${err instanceof Error ? err.message : String(err)}\n`);
313
377
  write(" → install drizzle-kit and ensure drizzle.config.ts exists\n");
@@ -402,7 +466,9 @@ export async function runMigrate(
402
466
  });
403
467
 
404
468
  const { overlay } = await resolveDrizzleKitEnv(cwd, loaded?.config, env);
469
+ const restore = applyDrizzleEnvOverlay(overlay);
405
470
  try {
471
+ await installOkeRlsHelpers(write);
406
472
  const code = await migrateFn({
407
473
  config: configPath,
408
474
  cwd,
@@ -417,6 +483,8 @@ export async function runMigrate(
417
483
  } catch (err) {
418
484
  write(`oke db migrate: ${err instanceof Error ? err.message : String(err)}\n`);
419
485
  return EXIT_RUNTIME;
486
+ } finally {
487
+ restore();
420
488
  }
421
489
  }
422
490
 
@@ -523,7 +591,7 @@ Not the same as \`oke schema generate\` (core/plugin stub tables).
523
591
  push Apply schema to the live local DB (dev; no migration files)
524
592
  generate Write versioned SQL under drizzle/ for review
525
593
  migrate Apply generated migrations (explicit; never automatic in prod)
526
- seed Run defineSeed (essential + env category); never at boot
594
+ seed Run defineSeed (essential + env category) standard CLI path
527
595
  studio Open drizzle-kit Studio (long-running)
528
596
 
529
597
  --env Override config env (dev|test|prod)
@@ -571,6 +639,39 @@ Not the same as \`oke schema generate\` (core/plugin stub tables).
571
639
  return runDb(sub, { config, env, force, entry });
572
640
  }
573
641
 
642
+ /**
643
+ * Install `oke.gate()` / `oke.user()` / `oke.has_scope()` before drizzle-kit
644
+ * so `CREATE POLICY … oke.gate()` succeeds.
645
+ *
646
+ * @param write - Output
647
+ */
648
+ async function installOkeRlsHelpers(write: (text: string) => void): Promise<void> {
649
+ const url = process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL;
650
+ const pgliteUrl = process.env.OKE_PGLITE_URL;
651
+ try {
652
+ if (pgliteUrl && !url) {
653
+ const { connectPglite } = await import("../drivers/pglite.ts");
654
+ const conn = await connectPglite({ url: pgliteUrl });
655
+ try {
656
+ await runOkeRlsHelperStatements((sql) => conn.exec(sql));
657
+ } finally {
658
+ await conn.close();
659
+ }
660
+ return;
661
+ }
662
+ if (!url) return;
663
+ const { connectPostgres } = await import("../drivers/postgres.ts");
664
+ const conn = await connectPostgres({ url });
665
+ try {
666
+ await runOkeRlsHelperStatements((sql) => conn.exec(sql));
667
+ } finally {
668
+ await conn.close();
669
+ }
670
+ } catch (err) {
671
+ write(`oke db: oke.* helpers skipped — ${err instanceof Error ? err.message : String(err)}\n`);
672
+ }
673
+ }
674
+
574
675
  function parseConfigEnv(value: string | undefined): ConfigEnv | undefined {
575
676
  if (value === "dev" || value === "test" || value === "prod") return value;
576
677
  // Soft-compat for older CLI flags / docs.
@@ -616,8 +717,19 @@ function reportKitResult(verb: string, result: DbKitResult, write: (text: string
616
717
  if (detail) write(detail);
617
718
  return EXIT_RUNTIME;
618
719
  }
619
- write(
620
- `oke db ${verb}: ${result.status}${result.error?.message ? ` — ${result.error.message}` : ""}\n`,
621
- );
720
+ write(`oke db ${verb}: ${result.status}${formatKitError(result.error)}\n`);
622
721
  return EXIT_RUNTIME;
623
722
  }
723
+
724
+ /**
725
+ * Format a drizzle-kit `error` object for the CLI line.
726
+ *
727
+ * @param error - Kit envelope error
728
+ */
729
+ export function formatKitError(error: DbKitResult["error"] | undefined): string {
730
+ if (!error) return "";
731
+ const parts = [error.code, error.message, error.sql].filter(
732
+ (part): part is string => typeof part === "string" && part.length > 0,
733
+ );
734
+ return parts.length > 0 ? ` — ${parts.join(" — ")}` : "";
735
+ }
@@ -14,6 +14,7 @@ describe("dev-controls", () => {
14
14
  expect(parseDevControlKey("?")).toBe("?");
15
15
  expect(parseDevControlKey("h")).toBe("?");
16
16
  expect(parseDevControlKey("r")).toBe("r");
17
+ expect(parseDevControlKey("s")).toBe("s");
17
18
  expect(parseDevControlKey("u")).toBe("u");
18
19
  expect(parseDevControlKey("x")).toBe("x");
19
20
  expect(parseDevControlKey("c")).toBeNull();
@@ -33,6 +34,9 @@ describe("dev-controls", () => {
33
34
  expect(formatDevControlsHint(false)).toContain("quit");
34
35
  expect(formatDevControlsHint(false)).not.toContain("services");
35
36
  expect(formatDevControlsHelp(false)).toContain("refresh");
37
+ expect(formatDevControlsHint(false)).toContain("seed");
38
+ expect(formatDevControlsHint(false)).not.toContain("restart");
39
+ expect(formatDevControlsHelp(false)).toContain("oke db seed");
36
40
  expect(formatDevControlsHelp(false)).not.toContain("select service");
37
41
  });
38
42
 
@@ -77,6 +81,22 @@ describe("dev-controls", () => {
77
81
  d.stop();
78
82
  });
79
83
 
84
+ test("dispatcher s runs onSeed", async () => {
85
+ let seeded = 0;
86
+ const d = createDevControlDispatcher({
87
+ write: () => {},
88
+ onQuit: () => {},
89
+ onSeed: () => {
90
+ seeded += 1;
91
+ },
92
+ composeAction: async () => {},
93
+ });
94
+ d.handleKey("s");
95
+ await Bun.sleep(10);
96
+ expect(seeded).toBe(1);
97
+ d.stop();
98
+ });
99
+
80
100
  test("busy compose action ignores a concurrent key press", async () => {
81
101
  let running = 0;
82
102
  let maxConcurrent = 0;
@@ -30,6 +30,10 @@ export type StartDevControlsOptions = {
30
30
  * After a compose up/stop — sync health into the board.
31
31
  */
32
32
  readonly onComposeSettled?: () => void | Promise<void>;
33
+ /**
34
+ * Run `oke db seed` (tests / non-Ink dispatcher).
35
+ */
36
+ readonly onSeed?: () => void | Promise<void>;
33
37
  /**
34
38
  * Run a compose control action against the whole stack.
35
39
  *
@@ -50,6 +54,7 @@ export function formatDevControlsHint(color: boolean = termColorEnabled()): stri
50
54
  `${s.dim}│${s.reset} ${s.dim}keys${s.reset} ` +
51
55
  `${s.cyan}?${s.reset} help · ` +
52
56
  `${s.cyan}r${s.reset} refresh · ` +
57
+ `${s.cyan}s${s.reset} seed · ` +
53
58
  `${s.cyan}q${s.reset} quit · ` +
54
59
  `${s.cyan}u${s.reset} up · ` +
55
60
  `${s.cyan}x${s.reset} stop` +
@@ -72,6 +77,7 @@ export function formatDevControlsHelp(color: boolean = termColorEnabled()): stri
72
77
  `${s.green}◇${s.reset} ${s.bold}Keys${s.reset}`,
73
78
  k("?", "help"),
74
79
  k("r", "refresh — clear logs, show latest ●"),
80
+ k("s", "oke db seed"),
75
81
  k("q", "quit oke dev"),
76
82
  k("u", "compose up -d (all)"),
77
83
  k("x", "compose stop (all)"),
@@ -140,6 +146,13 @@ export function createDevControlDispatcher(opts: StartDevControlsOptions): DevCo
140
146
  void Promise.resolve(opts.onShowPanel?.(body) ?? opts.write(body));
141
147
  return;
142
148
  }
149
+ if (key === "s") {
150
+ void Promise.resolve(opts.onSeed?.()).catch((err: unknown) => {
151
+ const msg = err instanceof Error ? err.message : String(err);
152
+ opts.write(formatStatusLine(`seed failed — ${msg}`, color, "error"));
153
+ });
154
+ return;
155
+ }
143
156
  if (key === "u" || key === "x") {
144
157
  void runAction(key === "u" ? "up" : "stop");
145
158
  }
@@ -19,7 +19,7 @@ import type { Manifest } from "../manifest/types.ts";
19
19
  import type { WideEvent } from "../runs/types.ts";
20
20
  import { isDataEnvelope, MCP_DATA_KIND } from "../mcp/data.ts";
21
21
  import { mintMcpSession } from "../mcp/session.ts";
22
- import { runDev, type DevOptions, type DevSession } from "./dev.ts";
22
+ import { isFlowsTreeWatchPath, runDev, type DevOptions, type DevSession } from "./dev.ts";
23
23
  import { mcpContextFromConsole } from "./mcp-from-console.ts";
24
24
 
25
25
  /** Repo public entry — absolute import so temp apps need no install. */
@@ -519,11 +519,11 @@ async function writePingApp(dir: string, version: string): Promise<void> {
519
519
  join(dir, "src/flows/ping.ts"),
520
520
  `import { on, flow, http, gate } from ${JSON.stringify(OKE_INDEX)};
521
521
 
522
- export const ping = on(http.get("/ping").gate.public, flow("ping", {
522
+ export const ping = on(http.get("/ping").public(), flow("ping", {
523
523
  do: () => ({ version: ${JSON.stringify(version)} as const }),
524
524
  }));
525
525
 
526
- export const slow = on(http.get("/slow").gate.public, flow("slow", {
526
+ export const slow = on(http.get("/slow").public(), flow("slow", {
527
527
  do: async () => {
528
528
  const started = ${JSON.stringify(version)};
529
529
  await Bun.sleep(800);
@@ -725,7 +725,8 @@ describe("oke dev syncAdoptBarrel atomic write", () => {
725
725
 
726
726
  const barrelPath = join(dir, "src/flows", ADOPT_BARREL_FILE);
727
727
  const text = await Bun.file(barrelPath).text();
728
- expect(text).toContain('export * as notes from "./notes/index.ts";');
728
+ expect(text).toContain('import * as notes from "./notes/index.ts";');
729
+ expect(text).toContain("export { notes }");
729
730
  expect(text).not.toContain("stale stub");
730
731
  expect(await Bun.file(`${barrelPath}.tmp`).exists()).toBe(false);
731
732
  const leftovers = (await readdir(join(dir, "src/flows"))).filter((f) => f.includes(".tmp"));
@@ -789,6 +790,16 @@ describe("oke dev syncAdoptBarrel atomic write", () => {
789
790
  });
790
791
  });
791
792
 
793
+ describe("isFlowsTreeWatchPath", () => {
794
+ test("flows tree files trigger regen; generated.ts does not", () => {
795
+ expect(isFlowsTreeWatchPath("flows/notes/[id]/get.ts")).toBe(true);
796
+ expect(isFlowsTreeWatchPath("flows\\notes\\list.ts")).toBe(true);
797
+ expect(isFlowsTreeWatchPath("flows/generated.ts")).toBe(false);
798
+ expect(isFlowsTreeWatchPath("flows/generated.ts.tmp")).toBe(false);
799
+ expect(isFlowsTreeWatchPath("core.ts")).toBe(false);
800
+ });
801
+ });
802
+
792
803
  describe("oke dev Docker-first", () => {
793
804
  test("dryRun always plans compose (no mode ask)", async () => {
794
805
  const dir = await mkdtemp(join(tmpdir(), "oke-dev-always-compose-"));