okengine 0.14.1 → 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 (205) hide show
  1. package/package.json +3 -4
  2. package/site/content/docs/elements/ai.mdx +2 -1
  3. package/site/content/docs/elements/channel.mdx +1 -0
  4. package/site/content/docs/elements/clock.mdx +3 -0
  5. package/site/content/docs/elements/flow.mdx +3 -0
  6. package/site/content/docs/elements/gate.mdx +25 -22
  7. package/site/content/docs/elements/signal.mdx +4 -4
  8. package/site/content/docs/elements/store.mdx +86 -27
  9. package/site/content/docs/elements/vault.mdx +1 -0
  10. package/site/content/docs/get-started/basic-usage.mdx +22 -13
  11. package/site/content/docs/get-started/index.mdx +5 -0
  12. package/site/content/docs/get-started/introduction.mdx +11 -3
  13. package/site/content/docs/get-started/meta.json +1 -1
  14. package/site/content/docs/get-started/project-structure.mdx +932 -0
  15. package/site/content/docs/reference/cli.md +13 -10
  16. package/site/content/docs/reference/client.mdx +25 -21
  17. package/site/content/docs/reference/security.md +1 -1
  18. package/src/auth/bindings.ts +14 -5
  19. package/src/auth/cross-plane.ts +10 -3
  20. package/src/cli/ai-setup/ai-setup.test.ts +62 -0
  21. package/src/cli/ai-setup/apply.ts +32 -1
  22. package/src/cli/attach-host-console.test.ts +3 -2
  23. package/src/cli/attach-host-console.ts +3 -0
  24. package/src/cli/build.test.ts +4 -2
  25. package/src/cli/db-seed.ts +48 -15
  26. package/src/cli/db.test.ts +104 -0
  27. package/src/cli/db.ts +122 -10
  28. package/src/cli/dev-controls.test.ts +20 -0
  29. package/src/cli/dev-controls.ts +13 -0
  30. package/src/cli/dev.test.ts +15 -4
  31. package/src/cli/dev.ts +102 -29
  32. package/src/cli/ensure-drizzle-config.ts +2 -1
  33. package/src/cli/tui/DevLive.tsx +12 -2
  34. package/src/cli/tui/keys.test.ts +2 -1
  35. package/src/cli/tui/keys.ts +3 -2
  36. package/src/compiler/aot.test.ts +1 -1
  37. package/src/compiler/extract.test.ts +409 -33
  38. package/src/compiler/extract.ts +318 -67
  39. package/src/compiler/flow-path.test.ts +96 -0
  40. package/src/compiler/flow-path.ts +173 -0
  41. package/src/compiler/generate-adopt.test.ts +112 -3
  42. package/src/compiler/generate-adopt.ts +280 -26
  43. package/src/compiler/schema-from-ast.ts +609 -0
  44. package/src/console/server/bind.ts +2 -2
  45. package/src/console/server/console.test.ts +3 -0
  46. package/src/console/server/flows.ts +120 -40
  47. package/src/console/server/invoke-user-flow.test.ts +3 -1
  48. package/src/console/server/invoke-user-flow.ts +17 -2
  49. package/src/console/server/iso-at.test.ts +9 -0
  50. package/src/console/server/iso-at.ts +11 -0
  51. package/src/console/server/security-headers.ts +6 -3
  52. package/src/console/server/spa-proxy.test.ts +1 -0
  53. package/src/console/server/sql-catalog.ts +147 -7
  54. package/src/console/server/sql-rls.test.ts +16 -0
  55. package/src/console/server/state.ts +8 -0
  56. package/src/console/server/store.test.ts +7 -4
  57. package/src/console/server/store.ts +142 -58
  58. package/src/console/ui-next/dist/assets/agent-disclosure-C0X1fbWF.js +1 -0
  59. package/src/console/ui-next/dist/assets/{cache-glyph-BanhLsEY.js → cache-glyph-F1FI122b.js} +1 -1
  60. package/src/console/ui-next/dist/assets/{call-pii-button-bqkxMrJH.js → call-pii-button-DEDCl_j3.js} +1 -1
  61. package/src/console/ui-next/dist/assets/collapsible-LPqGvfoz.js +1 -0
  62. package/src/console/ui-next/dist/assets/{confirm-sheet-DDCRmG62.js → confirm-sheet-3ptDzXbz.js} +1 -1
  63. package/src/console/ui-next/dist/assets/{duration-tone-oiRxPVsZ.js → duration-tone-sC3lGABz.js} +1 -1
  64. package/src/console/ui-next/dist/assets/{explorer-empty-HjTnVQoR.js → explorer-empty-BvYrygyO.js} +1 -1
  65. package/src/console/ui-next/dist/assets/flows-page-Dluo1Bd7.js +1 -0
  66. package/src/console/ui-next/dist/assets/{highlighted-json-CvDPvveV.js → highlighted-json-CS_O8L-r.js} +1 -1
  67. package/src/console/ui-next/dist/assets/http-method--sWDdXSB.js +1 -0
  68. package/src/console/ui-next/dist/assets/index-Ca3HZMVq.js +66 -0
  69. package/src/console/ui-next/dist/assets/index-UtTDRQpS.css +2 -0
  70. package/src/console/ui-next/dist/assets/{link-DF7SZ9Ek.js → link-COeyggt-.js} +1 -1
  71. package/src/console/ui-next/dist/assets/{observability-page-BEZDzyYh.js → observability-page-HK9-BO8z.js} +2 -2
  72. package/src/console/ui-next/dist/assets/react-D8E3mtu1.js +1 -0
  73. package/src/console/ui-next/dist/assets/replica-lag-DBIFFf7d.js +18 -0
  74. package/src/console/ui-next/dist/assets/sheet-form-Dnwa5oOR.js +1 -0
  75. package/src/console/ui-next/dist/assets/shortcut-keys-CoqwEIi0.js +1 -0
  76. package/src/console/ui-next/dist/assets/skeleton-BHcfDYcb.js +1 -0
  77. package/src/console/ui-next/dist/assets/store-page-BSohH3wM.js +41 -0
  78. package/src/console/ui-next/dist/assets/trace-detail-sheet-CFIGRnnA.js +2 -0
  79. package/src/console/ui-next/dist/assets/tree-expand-toggle-BgLl34w7.js +54 -0
  80. package/src/console/ui-next/dist/assets/units-page-B0cFE76A.js +1 -0
  81. package/src/console/ui-next/dist/assets/{use-vault-list-Cl79j_ku.js → use-vault-list-BLbZhzyF.js} +1 -1
  82. package/src/console/ui-next/dist/assets/vault-page-B1SbYe10.js +2 -0
  83. package/src/console/ui-next/dist/index.html +6 -6
  84. package/src/console/ui-next/src/client.ts +35 -7
  85. package/src/console/ui-next/src/components/motion/table/index.tsx +16 -0
  86. package/src/console/ui-next/src/components/motion/table/types.ts +4 -0
  87. package/src/console/ui-next/src/components/ui/kbd.tsx +2 -2
  88. package/src/console/ui-next/src/features/flows/traces/http-method.ts +19 -0
  89. package/src/console/ui-next/src/features/flows/traces/trace-detail.test.ts +19 -1
  90. package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +10 -3
  91. package/src/console/ui-next/src/features/store/detail/browse-section.tsx +45 -1
  92. package/src/console/ui-next/src/features/store/detail/resource-panel.tsx +78 -16
  93. package/src/console/ui-next/src/features/store/detail/store-row-detail-sheet.tsx +155 -14
  94. package/src/console/ui-next/src/features/store/explorer/store-tree.tsx +33 -133
  95. package/src/console/ui-next/src/features/store/files/file-preview.tsx +4 -1
  96. package/src/console/ui-next/src/features/store/grid/rls-policy-sheet.tsx +326 -165
  97. package/src/console/ui-next/src/features/store/grid/sql-insert-sheet.tsx +6 -0
  98. package/src/console/ui-next/src/features/store/grid/store-data-grid.tsx +50 -3
  99. package/src/console/ui-next/src/features/store/lib/files-meta.test.ts +3 -0
  100. package/src/console/ui-next/src/features/store/lib/files-meta.ts +12 -0
  101. package/src/console/ui-next/src/features/store/lib/query-defaults.test.ts +25 -0
  102. package/src/console/ui-next/src/features/store/lib/query-defaults.ts +85 -3
  103. package/src/console/ui-next/src/features/store/lib/query-gate.ts +30 -4
  104. package/src/console/ui-next/src/features/store/lib/rls-policy.test.ts +139 -0
  105. package/src/console/ui-next/src/features/store/lib/rls-policy.ts +223 -21
  106. package/src/console/ui-next/src/features/store/lib/schema-graph.ts +4 -0
  107. package/src/console/ui-next/src/features/store/lib/sql-catalog.test.ts +24 -0
  108. package/src/console/ui-next/src/features/store/lib/sql-catalog.ts +19 -0
  109. package/src/console/ui-next/src/features/store/lib/store-tree.test.ts +6 -0
  110. package/src/console/ui-next/src/features/store/lib/store-tree.ts +13 -0
  111. package/src/console/ui-next/src/features/store/query/query-console.tsx +48 -14
  112. package/src/console/ui-next/src/features/store/query/query-gate-menu.tsx +19 -20
  113. package/src/console/ui-next/src/features/store/query/query-gate-parts.tsx +2 -1
  114. package/src/console/ui-next/src/features/store/query/query-results.tsx +110 -83
  115. package/src/console/ui-next/src/features/store/schema/schema-table-node.tsx +17 -0
  116. package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +112 -72
  117. package/src/console/ui-next/src/features/units/lib/contract-input.test.ts +206 -1
  118. package/src/console/ui-next/src/features/units/lib/contract-input.ts +55 -0
  119. package/src/console/ui-next/src/features/vault/lib/types.ts +1 -1
  120. package/src/docker/compose-health.test.ts +27 -0
  121. package/src/docker/compose-health.ts +19 -3
  122. package/src/docker/docker.test.ts +3 -0
  123. package/src/docker/recipes/pgdog.ts +2 -0
  124. package/src/drivers/clock-postgres.ts +13 -9
  125. package/src/drivers/instances-postgres.ts +4 -6
  126. package/src/drivers/journal-postgres.ts +13 -9
  127. package/src/drivers/pg-rls.test.ts +87 -0
  128. package/src/drivers/pg-rls.ts +233 -7
  129. package/src/drivers/pglite.ts +26 -3
  130. package/src/drivers/postgres.test.ts +23 -0
  131. package/src/drivers/postgres.ts +133 -9
  132. package/src/drivers/types.ts +10 -0
  133. package/src/elements/clock/declare.ts +26 -1
  134. package/src/elements/gate/boot.ts +5 -5
  135. package/src/elements/gate/declare.ts +36 -4
  136. package/src/elements/gate/flatten.ts +1 -1
  137. package/src/elements/gate/runtime.ts +1 -1
  138. package/src/elements/store/declare.ts +9 -6
  139. package/src/elements/store/emit-drizzle.ts +44 -8
  140. package/src/elements/store/resource.test.ts +1 -1
  141. package/src/elements/store/rls-identity.test.ts +95 -0
  142. package/src/elements/store/rls-identity.ts +116 -0
  143. package/src/elements/store/runtime.ts +4 -0
  144. package/src/elements/store/schema-decl.test.ts +37 -0
  145. package/src/elements/store/schema-decl.ts +161 -0
  146. package/src/elements/store/seed.ts +1 -1
  147. package/src/elements/store/sql-condition.test.ts +12 -0
  148. package/src/elements/store/sql-condition.ts +26 -2
  149. package/src/elements/store/sql-rls-isolation.test.ts +165 -0
  150. package/src/elements/store/sql-rls-stamp.test.ts +117 -0
  151. package/src/elements/store/sql-session.ts +81 -3
  152. package/src/elements/store/upsert-app.test.ts +1 -1
  153. package/src/elements/store.ts +14 -0
  154. package/src/http.ts +20 -3
  155. package/src/index.ts +10 -1
  156. package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
  157. package/src/kernel/adopt-routes.ts +30 -2
  158. package/src/kernel/app.ts +131 -14
  159. package/src/kernel/auto-cache.test.ts +8 -8
  160. package/src/kernel/auto-registry.test.ts +49 -9
  161. package/src/kernel/boot.test.ts +4 -4
  162. package/src/kernel/boot.ts +2 -2
  163. package/src/kernel/effects-stamping.test.ts +3 -3
  164. package/src/kernel/element-registries.ts +12 -5
  165. package/src/kernel/errors.ts +24 -0
  166. package/src/kernel/flow-units.ts +64 -0
  167. package/src/kernel/flow.ts +29 -17
  168. package/src/kernel/fx.ts +23 -0
  169. package/src/kernel/horizontal-child.ts +4 -4
  170. package/src/kernel/http-path-pending.ts +20 -0
  171. package/src/kernel/http-resource.ts +4 -0
  172. package/src/kernel/http-stream.test.ts +4 -4
  173. package/src/kernel/pipeline.test.ts +2 -2
  174. package/src/kernel/plugin-elements.test.ts +1 -1
  175. package/src/kernel/plugin-needs.test.ts +1 -1
  176. package/src/kernel/ready.test.ts +2 -2
  177. package/src/kernel/resource-mount.test.ts +14 -0
  178. package/src/kernel/run-duration.test.ts +2 -2
  179. package/src/kernel/stamp-http.test.ts +79 -0
  180. package/src/kernel/stamp-http.ts +75 -0
  181. package/src/kernel/triggers.ts +52 -27
  182. package/src/manifest/types.ts +13 -0
  183. package/src/release/measure.ts +2 -2
  184. package/src/runtime/json-code-block.test.ts +2 -2
  185. package/src/runtime/serve.test.ts +4 -4
  186. package/src/term.test.ts +52 -4
  187. package/src/term.ts +125 -17
  188. package/src/test/reset-element-registries.ts +8 -3
  189. package/src/cli/ask-seed.test.ts +0 -96
  190. package/src/cli/ask-seed.ts +0 -82
  191. package/src/console/ui-next/dist/assets/collapsible-DYb0xU8C.js +0 -1
  192. package/src/console/ui-next/dist/assets/flows-page-DxDsOd4f.js +0 -1
  193. package/src/console/ui-next/dist/assets/http-method-CJCBYL2j.js +0 -1
  194. package/src/console/ui-next/dist/assets/index-Ce6WKWKM.js +0 -66
  195. package/src/console/ui-next/dist/assets/index-D4Ldtj79.css +0 -2
  196. package/src/console/ui-next/dist/assets/query-gate-parts-1m8m1iNp.js +0 -2
  197. package/src/console/ui-next/dist/assets/react-B1ML8gxg.js +0 -1
  198. package/src/console/ui-next/dist/assets/replica-lag-C8_BRt2x.js +0 -18
  199. package/src/console/ui-next/dist/assets/sheet-form-D-ata7jy.js +0 -1
  200. package/src/console/ui-next/dist/assets/shortcut-keys-DO4IsVqv.js +0 -1
  201. package/src/console/ui-next/dist/assets/skeleton-CL_X0GCj.js +0 -1
  202. package/src/console/ui-next/dist/assets/store-page-v3LXdYpr.js +0 -45
  203. package/src/console/ui-next/dist/assets/trace-detail-sheet-DFLFfUUX.js +0 -2
  204. package/src/console/ui-next/dist/assets/units-page-C0gW6Kdo.js +0 -1
  205. package/src/console/ui-next/dist/assets/vault-page-DuKzqwzW.js +0 -2
@@ -0,0 +1,96 @@
1
+ /**
2
+ * File-tree path stamps — POSIX normalize is a requirement, not an assumption.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import {
7
+ importSpecifierFromWalked,
8
+ nameFromFlowFile,
9
+ pathFromFlowFile,
10
+ toPosixPath,
11
+ unitFromFlowFile,
12
+ } from "./flow-path.ts";
13
+
14
+ describe("toPosixPath / importSpecifierFromWalked", () => {
15
+ test("normalizes Windows backslashes to /", () => {
16
+ expect(toPosixPath("notes\\[id]\\get.ts")).toBe("notes/[id]/get.ts");
17
+ });
18
+
19
+ test("emits a POSIX import specifier even when the walked path used \\", () => {
20
+ const spec = importSpecifierFromWalked("notes\\[id]\\get.ts");
21
+ expect(spec).toBe("./notes/[id]/get.ts");
22
+ expect(spec.includes("\\")).toBe(false);
23
+ });
24
+
25
+ test("catch-all walked with \\ becomes a quoted-ready POSIX specifier", () => {
26
+ expect(importSpecifierFromWalked("docs\\[...slug]\\get.ts")).toBe("./docs/[...slug]/get.ts");
27
+ });
28
+ });
29
+
30
+ describe("pathFromFlowFile", () => {
31
+ test("[id] → :id on a reserved get leaf", () => {
32
+ expect(pathFromFlowFile("notes/[id]/get.ts")).toBe("/notes/:id");
33
+ });
34
+
35
+ test("[...slug] → *", () => {
36
+ expect(pathFromFlowFile("docs/[...slug]/get.ts")).toBe("/docs/*");
37
+ });
38
+
39
+ test("(group) is omitted from the URL", () => {
40
+ expect(pathFromFlowFile("notes/(ops)/archive.ts")).toBe("/notes/archive");
41
+ });
42
+
43
+ test("reserved leaves do not add a segment", () => {
44
+ expect(pathFromFlowFile("notes/list.ts")).toBe("/notes");
45
+ expect(pathFromFlowFile("notes/create.ts")).toBe("/notes");
46
+ expect(pathFromFlowFile("notes/route.ts")).toBe("/notes");
47
+ expect(pathFromFlowFile("notes/[id]/update.ts")).toBe("/notes/:id");
48
+ expect(pathFromFlowFile("notes/[id]/remove.ts")).toBe("/notes/:id");
49
+ });
50
+
51
+ test("action leaves add a segment", () => {
52
+ expect(pathFromFlowFile("notes/[id]/archive.ts")).toBe("/notes/:id/archive");
53
+ expect(pathFromFlowFile("notes/query.ts")).toBe("/notes/query");
54
+ });
55
+
56
+ test("main omits the URL prefix — health.ts is /health, index/route is /", () => {
57
+ expect(pathFromFlowFile("main/health.ts")).toBe("/health");
58
+ expect(pathFromFlowFile("main/index.ts")).toBe("/");
59
+ expect(pathFromFlowFile("main/route.ts")).toBe("/");
60
+ });
61
+
62
+ test("skip-list files return undefined", () => {
63
+ expect(pathFromFlowFile("notes/shapes.ts")).toBeUndefined();
64
+ expect(pathFromFlowFile("notes/signals.ts")).toBeUndefined();
65
+ expect(pathFromFlowFile("notes/list.test.ts")).toBeUndefined();
66
+ expect(pathFromFlowFile("notes/_lib/util.ts")).toBeUndefined();
67
+ expect(pathFromFlowFile("src/flows/generated.ts")).toBeUndefined();
68
+ });
69
+
70
+ test("accepts a source path under src/flows/", () => {
71
+ expect(pathFromFlowFile("src/flows/notes/[id]/get.ts")).toBe("/notes/:id");
72
+ });
73
+
74
+ test("Windows-shaped string yields the same URL as POSIX (requirement)", () => {
75
+ expect(pathFromFlowFile("notes\\[id]\\get.ts")).toBe("/notes/:id");
76
+ expect(pathFromFlowFile("notes\\[...slug]\\get.ts")).toBe("/notes/*");
77
+ expect(pathFromFlowFile("src\\flows\\notes\\[id]\\get.ts")).toBe("/notes/:id");
78
+ });
79
+
80
+ test("[[...slug]] optional catch-all is not inferred", () => {
81
+ expect(pathFromFlowFile("docs/[[...slug]]/get.ts")).toBeUndefined();
82
+ });
83
+ });
84
+
85
+ describe("nameFromFlowFile / unitFromFlowFile", () => {
86
+ test("unit + export — [id] is not part of the name", () => {
87
+ expect(unitFromFlowFile("src/flows/notes/[id]/get.ts")).toBe("notes");
88
+ expect(nameFromFlowFile("src/flows/notes/[id]/get.ts", "get")).toBe("notes.get");
89
+ expect(nameFromFlowFile("notes/[id]/archive.ts", "archive")).toBe("notes.archive");
90
+ });
91
+
92
+ test("main unit keeps the main. prefix on the Flow name", () => {
93
+ expect(nameFromFlowFile("main/health.ts", "health")).toBe("main.health");
94
+ expect(nameFromFlowFile("main/route.ts", "root")).toBe("main.root");
95
+ });
96
+ });
@@ -0,0 +1,173 @@
1
+ /**
2
+ * File-tree URL + Flow-name stamps.
3
+ *
4
+ * One catalog for the adopt generator and Manifest extract — never derive a
5
+ * URL from `flow("notes.get")`. Always POSIX-normalize first; Windows `\`
6
+ * must not leak into inferred paths or generated import specifiers.
7
+ */
8
+
9
+ /** Reserved leaves that do not add a URL segment (same five as `http.resource`, plus `index` / `route`). */
10
+ export const RESERVED_LEAVES: ReadonlySet<string> = new Set([
11
+ "list",
12
+ "create",
13
+ "get",
14
+ "update",
15
+ "remove",
16
+ "index",
17
+ "route",
18
+ ]);
19
+
20
+ /** Filenames that are never routes. */
21
+ export const SKIP_FLOW_FILENAMES: ReadonlySet<string> = new Set([
22
+ "generated.ts",
23
+ "shapes.ts",
24
+ "signals.ts",
25
+ ]);
26
+
27
+ /**
28
+ * Convert a filesystem path to POSIX `/` separators.
29
+ *
30
+ * Requirement, not an assumption: `node:path` `join` / `relative` emit `\`
31
+ * on Windows. ESM specifiers and URL stamps are always `/`.
32
+ *
33
+ * @param path - Walked or hand-built path (may contain `\`)
34
+ */
35
+ export function toPosixPath(path: string): string {
36
+ return path.replace(/\\/g, "/");
37
+ }
38
+
39
+ /**
40
+ * Build an ESM import specifier from a path walked relative to `src/flows`.
41
+ *
42
+ * Always POSIX — even when `walked` used `\`.
43
+ *
44
+ * @param walked - Relative path from the flows directory
45
+ */
46
+ export function importSpecifierFromWalked(walked: string): string {
47
+ const posix = toPosixPath(walked).replace(/^\.?\//, "");
48
+ return `./${posix}`;
49
+ }
50
+
51
+ /**
52
+ * True when a file (or its basename) is on the skip list.
53
+ *
54
+ * @param fileName - Basename or relative path
55
+ */
56
+ export function isSkipFlowFile(fileName: string): boolean {
57
+ const base = toPosixPath(fileName).split("/").pop() ?? fileName;
58
+ if (base.startsWith("_")) return true;
59
+ if (SKIP_FLOW_FILENAMES.has(base)) return true;
60
+ if (base.endsWith(".test.ts") || base.endsWith(".test.tsx")) return true;
61
+ return false;
62
+ }
63
+
64
+ /**
65
+ * Strip a source path down to the segment after `flows/`.
66
+ *
67
+ * Accepts `notes/[id]/get.ts`, `src/flows/notes/[id]/get.ts`, or an absolute
68
+ * path containing `/flows/`.
69
+ *
70
+ * @param input - Relative or absolute path
71
+ */
72
+ export function relFromFlowsDir(input: string): string {
73
+ const posix = toPosixPath(input).replace(/^\.?\//, "");
74
+ const marker = "/flows/";
75
+ const idx = posix.lastIndexOf(marker);
76
+ if (idx >= 0) return posix.slice(idx + marker.length);
77
+ if (posix.startsWith("flows/")) return posix.slice("flows/".length);
78
+ if (posix.startsWith("src/flows/")) return posix.slice("src/flows/".length);
79
+ return posix;
80
+ }
81
+
82
+ /**
83
+ * Infer the HTTP path from a flow file's location under `src/flows`.
84
+ *
85
+ * - `[id]` → `:id`, `[...slug]` → `*`
86
+ * - `(group)` omitted
87
+ * - reserved leaves omit their filename
88
+ * - `main` omits the first URL segment
89
+ * - skip-list files return `undefined`
90
+ *
91
+ * @param relFromFlowsDirOrSource - Path relative to flows or a source file path
92
+ */
93
+ export function pathFromFlowFile(relFromFlowsDirOrSource: string): string | undefined {
94
+ const posix = toPosixPath(relFromFlowsDirOrSource);
95
+ const rel = relFromFlowsDir(posix);
96
+ if (!rel) return undefined;
97
+
98
+ const parts = rel.split("/").filter(Boolean);
99
+ if (parts.length < 2) return undefined;
100
+
101
+ const fileName = parts[parts.length - 1]!;
102
+ if (isSkipFlowFile(fileName)) return undefined;
103
+ if (!/\.(ts|tsx|js|jsx)$/.test(fileName)) return undefined;
104
+
105
+ const leaf = fileName.replace(/\.(ts|tsx|js|jsx)$/, "");
106
+ const unit = parts[0]!;
107
+ const dirs = parts.slice(1, -1);
108
+
109
+ const segments: string[] = [];
110
+ if (unit !== "main") segments.push(unit);
111
+
112
+ for (const dir of dirs) {
113
+ if (dir.startsWith("_")) return undefined;
114
+ if (dir.startsWith("(") && dir.endsWith(")")) continue;
115
+ if (dir.startsWith("[[") && dir.endsWith("]]")) return undefined;
116
+ const mapped = mapSegment(dir);
117
+ if (mapped === undefined) return undefined;
118
+ segments.push(mapped);
119
+ }
120
+
121
+ if (!RESERVED_LEAVES.has(leaf)) {
122
+ const mapped = mapSegment(leaf);
123
+ if (mapped === undefined) return undefined;
124
+ segments.push(mapped);
125
+ }
126
+
127
+ if (segments.length === 0) return "/";
128
+ return `/${segments.join("/")}`;
129
+ }
130
+
131
+ /**
132
+ * Infer `unit.export` from a flow file + export name.
133
+ *
134
+ * `[id]` / `(group)` never enter the name.
135
+ *
136
+ * @param filePath - Source path under `flows/`
137
+ * @param exportName - `export const` binding
138
+ */
139
+ export function nameFromFlowFile(
140
+ filePath: string,
141
+ exportName: string | undefined,
142
+ ): string | undefined {
143
+ if (!exportName) return undefined;
144
+ const unit = unitFromFlowFile(filePath);
145
+ if (!unit) return undefined;
146
+ return `${unit}.${exportName}`;
147
+ }
148
+
149
+ /**
150
+ * First-level folder under `src/flows` — the client unit.
151
+ *
152
+ * @param filePath - Source path
153
+ */
154
+ export function unitFromFlowFile(filePath: string): string | undefined {
155
+ const rel = relFromFlowsDir(filePath);
156
+ const unit = rel.split("/").filter(Boolean)[0];
157
+ if (!unit) return undefined;
158
+ if (unit.startsWith("[") || unit.startsWith("(") || unit.startsWith("_")) return undefined;
159
+ if (unit.includes(".")) return undefined;
160
+ return unit;
161
+ }
162
+
163
+ /**
164
+ * Map one folder or leaf token to a URL segment.
165
+ *
166
+ * @param token - `[id]`, `[...slug]`, or a static name
167
+ */
168
+ function mapSegment(token: string): string | undefined {
169
+ if (token.startsWith("[[") && token.endsWith("]]")) return undefined;
170
+ if (token.startsWith("[...") && token.endsWith("]")) return "*";
171
+ if (token.startsWith("[") && token.endsWith("]")) return `:${token.slice(1, -1)}`;
172
+ return token;
173
+ }
@@ -6,7 +6,8 @@ import { describe, expect, test } from "bun:test";
6
6
  import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
7
7
  import { tmpdir } from "node:os";
8
8
  import { join } from "node:path";
9
- import { generateAdoptBarrel } from "./generate-adopt.ts";
9
+ import { importSpecifierFromWalked } from "./flow-path.ts";
10
+ import { generateAdoptBarrel, GenerateAdoptError, writeAdoptBarrel } from "./generate-adopt.ts";
10
11
 
11
12
  async function makeUnit(root: string, name: string, withIndex = true): Promise<void> {
12
13
  const dir = join(root, "src/flows", name);
@@ -22,8 +23,11 @@ describe("generateAdoptBarrel", () => {
22
23
  await makeUnit(root, "main");
23
24
  const result = await generateAdoptBarrel({ rootDir: root });
24
25
  expect(result.units).toEqual(["main", "notes"]);
25
- expect(result.source).toContain('export * as main from "./main/index.ts";');
26
- expect(result.source).toContain('export * as notes from "./notes/index.ts";');
26
+ expect(result.source).toContain('import * as main from "./main/index.ts";');
27
+ expect(result.source).toContain('import * as notes from "./notes/index.ts";');
28
+ expect(result.source).toContain("export { main };");
29
+ expect(result.source).toContain("export { notes };");
30
+ expect(result.source).toContain("registerFlowUnits({ main, notes })");
27
31
  expect(result.skipped).toEqual([]);
28
32
  } finally {
29
33
  await rm(root, { recursive: true, force: true });
@@ -66,6 +70,15 @@ describe("generateAdoptBarrel", () => {
66
70
  );
67
71
  const result = await generateAdoptBarrel({ rootDir: root });
68
72
  const generatedPath = join(root, "src/flows/generated.ts");
73
+ await mkdir(join(root, "node_modules/okengine"), { recursive: true });
74
+ await writeFile(
75
+ join(root, "node_modules/okengine/package.json"),
76
+ JSON.stringify({ name: "okengine", type: "module", exports: "./index.ts" }),
77
+ );
78
+ await writeFile(
79
+ join(root, "node_modules/okengine/index.ts"),
80
+ "export function registerFlowUnits() {}\n",
81
+ );
69
82
  await writeFile(generatedPath, result.source);
70
83
 
71
84
  // Same shape as `import * as main from "./flows/main"; import * as
@@ -82,4 +95,100 @@ describe("generateAdoptBarrel", () => {
82
95
  await rm(root, { recursive: true, force: true });
83
96
  }
84
97
  });
98
+
99
+ test("tree unit synthesizes a namespace and POSIX-quoted [id] imports", async () => {
100
+ const root = await mkdtemp(join(tmpdir(), "oke-adopt-gen-"));
101
+ try {
102
+ const getDir = join(root, "src/flows/notes/[id]");
103
+ await mkdir(getDir, { recursive: true });
104
+ await writeFile(
105
+ join(root, "src/flows/notes/list.ts"),
106
+ 'export const list = { name: "notes.list" };\n',
107
+ );
108
+ await writeFile(join(getDir, "get.ts"), 'export const get = { name: "notes.get" };\n');
109
+ const result = await generateAdoptBarrel({ rootDir: root });
110
+ expect(result.units).toEqual(["notes"]);
111
+ expect(result.source).toContain('from "./notes/[id]/get.ts"');
112
+ expect(result.source).toContain('from "./notes/list.ts"');
113
+ expect(result.source.includes("\\")).toBe(false);
114
+ expect(result.source).toContain("stampHttpPath");
115
+ expect(result.source).toContain('"/notes/:id"');
116
+ expect(result.source).toContain('"/notes"');
117
+ } finally {
118
+ await rm(root, { recursive: true, force: true });
119
+ }
120
+ });
121
+
122
+ test("mixed barrel index.ts + tree files is a generate error", async () => {
123
+ const root = await mkdtemp(join(tmpdir(), "oke-adopt-gen-"));
124
+ try {
125
+ await mkdir(join(root, "src/flows/notes/[id]"), { recursive: true });
126
+ await writeFile(join(root, "src/flows/notes/index.ts"), "export const list = 1;\n");
127
+ await writeFile(join(root, "src/flows/notes/[id]/get.ts"), "export const get = 1;\n");
128
+ await expect(generateAdoptBarrel({ rootDir: root })).rejects.toBeInstanceOf(
129
+ GenerateAdoptError,
130
+ );
131
+ } finally {
132
+ await rm(root, { recursive: true, force: true });
133
+ }
134
+ });
135
+
136
+ test("two files exporting the same name is a generate error", async () => {
137
+ const root = await mkdtemp(join(tmpdir(), "oke-adopt-gen-"));
138
+ try {
139
+ await mkdir(join(root, "src/flows/notes"), { recursive: true });
140
+ await writeFile(join(root, "src/flows/notes/list.ts"), "export const get = 1;\n");
141
+ await writeFile(join(root, "src/flows/notes/route.ts"), "export const get = 2;\n");
142
+ await expect(generateAdoptBarrel({ rootDir: root })).rejects.toThrow(/exports "get"/);
143
+ } finally {
144
+ await rm(root, { recursive: true, force: true });
145
+ }
146
+ });
147
+
148
+ test("flow('tasks.get') under notes/ is a generate error", async () => {
149
+ const root = await mkdtemp(join(tmpdir(), "oke-adopt-gen-"));
150
+ try {
151
+ await mkdir(join(root, "src/flows/notes"), { recursive: true });
152
+ await writeFile(
153
+ join(root, "src/flows/notes/get.ts"),
154
+ 'export const get = flow("tasks.get", { do: () => 1 });\n',
155
+ );
156
+ await expect(generateAdoptBarrel({ rootDir: root })).rejects.toThrow(
157
+ /does not match the folder/,
158
+ );
159
+ } finally {
160
+ await rm(root, { recursive: true, force: true });
161
+ }
162
+ });
163
+
164
+ test("writeAdoptBarrel skips identical bytes", async () => {
165
+ const root = await mkdtemp(join(tmpdir(), "oke-adopt-gen-"));
166
+ try {
167
+ await mkdir(join(root, "src/flows"), { recursive: true });
168
+ const source = "// same\n";
169
+ expect((await writeAdoptBarrel(root, source)).written).toBe(true);
170
+ expect((await writeAdoptBarrel(root, source)).written).toBe(false);
171
+ } finally {
172
+ await rm(root, { recursive: true, force: true });
173
+ }
174
+ });
175
+
176
+ test("import specifiers stay POSIX when the walked path used backslash", () => {
177
+ const spec = importSpecifierFromWalked("notes\\[id]\\get.ts");
178
+ expect(spec).toBe("./notes/[id]/get.ts");
179
+ expect(spec.includes("\\")).toBe(false);
180
+ });
181
+
182
+ test("[...slug] unit adds the * client-key comment", async () => {
183
+ const root = await mkdtemp(join(tmpdir(), "oke-adopt-gen-"));
184
+ try {
185
+ await mkdir(join(root, "src/flows/docs/[...slug]"), { recursive: true });
186
+ await writeFile(join(root, "src/flows/docs/[...slug]/get.ts"), "export const get = 1;\n");
187
+ const result = await generateAdoptBarrel({ rootDir: root });
188
+ expect(result.source).toContain('Catch-all param is "*"');
189
+ expect(result.source).toContain('from "./docs/[...slug]/get.ts"');
190
+ } finally {
191
+ await rm(root, { recursive: true, force: true });
192
+ }
193
+ });
85
194
  });