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
@@ -1,20 +1,30 @@
1
1
  /**
2
- * `.adopt()` route-barrel generator — the disk-file alternative to a
3
- * virtual-module Bun plugin (investigated and rejected: `Bun.plugin()`'s
2
+ * `.adopt()` / auto-register barrel generator — the disk-file alternative
3
+ * to a virtual-module Bun plugin (investigated and rejected: `Bun.plugin()`'s
4
4
  * `onResolve` is never invoked for an unresolvable specifier during a plain
5
5
  * runtime `import()`, so `oke dev` and `oke build` would resolve a virtual
6
6
  * module differently — measured, not assumed).
7
7
  *
8
- * A real file on disk is an ordinary import: identical resolution under
9
- * `oke dev`'s runtime `import()` and `oke build`'s `Bun.build()`. Convention
10
- * (validated against every current example app both `create-oke`
11
- * templates and the `skyport` compiler fixture, 7/7): every unit under
12
- * `<rootDir>/src/flows/*` has an `index.ts` barrel.
8
+ * Two unit shapes:
9
+ * - **Barrel** only `index.ts` (+ skip-list) `import * as notes from "./notes/index.ts"; export { notes }`
10
+ * - **Tree** — `[param]` / `(group)` / extra route files synthesized namespace
11
+ * with `stampHttpPath` / `stampFlowName` after import
12
+ *
13
+ * Mixed barrel `index.ts` + tree files is a generate error (ambiguous:
14
+ * `index` is also a reserved leaf).
13
15
  */
14
16
 
15
- import { mkdir, readdir, rename, writeFile } from "node:fs/promises";
17
+ import { mkdir, readdir, readFile, rename, writeFile } from "node:fs/promises";
16
18
  import { join } from "node:path";
17
19
 
20
+ import {
21
+ importSpecifierFromWalked,
22
+ isSkipFlowFile,
23
+ nameFromFlowFile,
24
+ pathFromFlowFile,
25
+ toPosixPath,
26
+ } from "./flow-path.ts";
27
+
18
28
  /** Options for {@link generateAdoptBarrel}. */
19
29
  export interface GenerateAdoptBarrelOptions {
20
30
  /** Project root (contains `src/flows`). */
@@ -29,13 +39,16 @@ export interface AdoptBarrelResult {
29
39
  readonly source: string;
30
40
  /** Unit (folder) names included, sorted. */
31
41
  readonly units: readonly string[];
32
- /** Folders under `flowsDir` skipped for lacking an `index.ts` barrel. */
42
+ /** Folders under `flowsDir` skipped (empty / invalid name / no routes). */
33
43
  readonly skipped: readonly string[];
34
44
  }
35
45
 
36
46
  const GENERATED_HEADER =
37
47
  "// AUTO-GENERATED by `oke dev` / `oke build` — do not edit by hand.\n" +
38
- "// Regenerated from every `src/flows/<unit>/index.ts` unit folder.\n";
48
+ "// Regenerated from every `src/flows/<unit>/` folder (barrel or file tree).\n";
49
+
50
+ const CATCHALL_COMMENT =
51
+ '// Catch-all param is "*" — call api.docs.get({ "*": "a/b/c" }), not { slug }.\n';
39
52
 
40
53
  /** Filename of the on-disk `.adopt()` barrel under the flows directory. */
41
54
  export const ADOPT_BARREL_FILE = "generated.ts";
@@ -43,11 +56,39 @@ export const ADOPT_BARREL_FILE = "generated.ts";
43
56
  /** A folder name usable as an `export * as <name>` binding. */
44
57
  const VALID_UNIT_NAME = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
45
58
 
59
+ const EXPORT_CONST = /^export const ([A-Za-z_$][A-Za-z0-9_$]*)\b/gm;
60
+
61
+ const EXPLICIT_FLOW_UNIT = /flow\(\s*["']([A-Za-z_$][A-Za-z0-9_$]*)\./g;
62
+
63
+ /**
64
+ * Generate-time failure (mixed barrel + tree, export collision, unit drift).
65
+ */
66
+ export class GenerateAdoptError extends Error {
67
+ /** Stable error name. */
68
+ override readonly name = "GenerateAdoptError";
69
+
70
+ /**
71
+ * @param message - Why generation failed
72
+ */
73
+ constructor(message: string) {
74
+ super(message);
75
+ }
76
+ }
77
+
78
+ /** One route file discovered under a unit folder. */
79
+ interface WalkedFlowFile {
80
+ /** POSIX path relative to the unit folder (`[id]/get.ts`). */
81
+ readonly relFromUnit: string;
82
+ /** File source (for export + drift scans). */
83
+ readonly source: string;
84
+ /** `export const` names. */
85
+ readonly exports: readonly string[];
86
+ }
87
+
46
88
  /**
47
89
  * Write the adopt barrel atomically: `generated.ts.tmp` → rename to
48
- * `generated.ts`. Concurrent readers (e.g. `bun --hot` racing a save) never
49
- * observe a torn/partial file — plain `writeFile` on the destination would
50
- * truncate first.
90
+ * `generated.ts`. Skips the write when bytes are unchanged so the `src/`
91
+ * watcher + bun `--hot` do not loop.
51
92
  *
52
93
  * @param rootDir - Project root
53
94
  * @param source - Full barrel source from {@link generateAdoptBarrel}
@@ -57,24 +98,28 @@ export async function writeAdoptBarrel(
57
98
  rootDir: string,
58
99
  source: string,
59
100
  flowsDir = "src/flows",
60
- ): Promise<void> {
101
+ ): Promise<{ written: boolean }> {
61
102
  const dir = join(rootDir, flowsDir);
62
103
  const target = join(dir, ADOPT_BARREL_FILE);
63
104
  const tmp = `${target}.tmp`;
64
105
  await mkdir(dir, { recursive: true });
106
+ try {
107
+ const existing = await readFile(target, "utf8");
108
+ if (existing === source) return { written: false };
109
+ } catch {
110
+ // missing — write
111
+ }
65
112
  await writeFile(tmp, source);
66
113
  await rename(tmp, target);
114
+ return { written: true };
67
115
  }
68
116
 
69
117
  /**
70
- * Scan `<rootDir>/<flowsDir>/*` and emit a barrel re-exporting every unit
71
- * folder's `index.ts` under its folder name — the same shape
72
- * `.adopt({ main, notes })` already takes by hand.
118
+ * Scan `<rootDir>/<flowsDir>/*` and emit a barrel that registers every unit.
73
119
  *
74
- * Never throws: a missing/unreadable flows directory yields an empty
75
- * barrel, same posture as the rest of the lazy AoT tooling (best-effort,
76
- * boot-time checks are what enforce staleness — see
77
- * `assertAdoptBarrelFresh` in `kernel/boot.ts`).
120
+ * Never throws for a missing/unreadable flows directory (empty barrel).
121
+ * Mixed barrel + tree, export collisions, and unit-prefix drift throw
122
+ * {@link GenerateAdoptError}.
78
123
  *
79
124
  * @param options - Root dir / flows dir override
80
125
  */
@@ -86,12 +131,17 @@ export async function generateAdoptBarrel(
86
131
 
87
132
  const units: string[] = [];
88
133
  const skipped: string[] = [];
134
+ const unitBlocks: string[] = [];
135
+ const registerNames: string[] = [];
136
+ const declareLines: string[] = [];
137
+ let hasCatchAll = false;
138
+ let needsStampImport = false;
139
+
89
140
  try {
90
141
  const dirents = await readdir(abs, { withFileTypes: true });
91
142
  for (const d of dirents.sort((a, b) => a.name.localeCompare(b.name))) {
92
143
  if (!d.isDirectory()) continue;
93
- const hasBarrel = await Bun.file(join(abs, d.name, "index.ts")).exists();
94
- if (!hasBarrel) {
144
+ if (d.name.startsWith("_")) {
95
145
  skipped.push(d.name);
96
146
  continue;
97
147
  }
@@ -99,13 +149,217 @@ export async function generateAdoptBarrel(
99
149
  skipped.push(d.name);
100
150
  continue;
101
151
  }
152
+
153
+ const unitAbs = join(abs, d.name);
154
+ const files = await listFlowFiles(unitAbs);
155
+ const hasIndex = files.some((f) => f.relFromUnit === "index.ts");
156
+ const hasOther = files.some((f) => f.relFromUnit !== "index.ts");
157
+
158
+ if (hasIndex && hasOther) {
159
+ throw new GenerateAdoptError(
160
+ `Unit "${d.name}" mixes a barrel index.ts with tree route files. ` +
161
+ `Use only index.ts (barrel), or move the collection path to route.ts ` +
162
+ `and keep [id]/ beside it.`,
163
+ );
164
+ }
165
+
166
+ if (files.length === 0) {
167
+ skipped.push(d.name);
168
+ continue;
169
+ }
170
+
102
171
  units.push(d.name);
172
+ registerNames.push(d.name);
173
+ declareLines.push(` readonly ${d.name}: typeof ${d.name};`);
174
+
175
+ if (unitBlocks.length > 0) unitBlocks.push("");
176
+
177
+ if (hasIndex && !hasOther) {
178
+ const spec = JSON.stringify(importSpecifierFromWalked(`${d.name}/index.ts`));
179
+ unitBlocks.push(`import * as ${d.name} from ${spec};`);
180
+ unitBlocks.push(`export { ${d.name} };`);
181
+ continue;
182
+ }
183
+
184
+ needsStampImport = true;
185
+ const { lines, catchAll } = emitTreeUnit(d.name, files);
186
+ if (catchAll) hasCatchAll = true;
187
+ unitBlocks.push(...lines);
103
188
  }
104
- } catch {
189
+ } catch (err) {
190
+ if (err instanceof GenerateAdoptError) throw err;
105
191
  // Missing flows dir — empty barrel, not a throw (mirrors extractManifest).
106
192
  }
107
193
 
108
- const lines = units.map((u) => `export * as ${u} from "./${u}/index.ts";`);
109
- const source = lines.length > 0 ? `${GENERATED_HEADER}${lines.join("\n")}\n` : GENERATED_HEADER;
194
+ const parts: string[] = [GENERATED_HEADER.trimEnd()];
195
+ if (hasCatchAll) parts.push(CATCHALL_COMMENT.trimEnd());
196
+ if (registerNames.length > 0) {
197
+ const stamp = needsStampImport ? ", stampFlowName, stampHttpPath" : "";
198
+ parts.push(`import { registerFlowUnits${stamp} } from "okengine";`);
199
+ }
200
+ if (unitBlocks.length > 0) {
201
+ parts.push(unitBlocks.join("\n"));
202
+ }
203
+ if (registerNames.length > 0) {
204
+ parts.push(`registerFlowUnits({ ${registerNames.join(", ")} });`);
205
+ parts.push('declare module "okengine" {');
206
+ parts.push(" interface RegisteredFlowUnits {");
207
+ parts.push(...declareLines);
208
+ parts.push(" }");
209
+ parts.push("}");
210
+ }
211
+
212
+ const source = `${parts.join("\n")}\n`;
110
213
  return { source, units, skipped };
111
214
  }
215
+
216
+ /**
217
+ * Walk a unit folder for route files (skip-list applied).
218
+ *
219
+ * @param unitAbs - Absolute unit directory
220
+ */
221
+ async function listFlowFiles(unitAbs: string): Promise<WalkedFlowFile[]> {
222
+ const out: WalkedFlowFile[] = [];
223
+
224
+ const walk = async (dir: string, rel: string): Promise<void> => {
225
+ const dirents = await readdir(dir, { withFileTypes: true });
226
+ for (const d of dirents) {
227
+ if (d.name.startsWith("_")) continue;
228
+ const posixName = toPosixPath(d.name);
229
+ if (d.isDirectory()) {
230
+ if (posixName.startsWith("[[") && posixName.endsWith("]]")) {
231
+ throw new GenerateAdoptError(
232
+ `Optional catch-all "${posixName}" is not supported — use [...slug] (param is "*").`,
233
+ );
234
+ }
235
+ await walk(join(dir, d.name), rel ? `${rel}/${posixName}` : posixName);
236
+ continue;
237
+ }
238
+ if (!posixName.endsWith(".ts") && !posixName.endsWith(".tsx")) continue;
239
+ if (isSkipFlowFile(posixName)) continue;
240
+ const relFile = rel ? `${rel}/${posixName}` : posixName;
241
+ const source = await readFile(join(dir, d.name), "utf8");
242
+ const exports = listExportedConsts(source);
243
+ const relPosix = toPosixPath(relFile);
244
+ // Barrel `index.ts` counts even with no `export const` (`export {}` / re-exports).
245
+ if (exports.length === 0 && relPosix !== "index.ts") continue;
246
+ out.push({ relFromUnit: relPosix, source, exports });
247
+ }
248
+ };
249
+
250
+ try {
251
+ await walk(unitAbs, "");
252
+ } catch (err) {
253
+ if (err instanceof GenerateAdoptError) throw err;
254
+ }
255
+
256
+ return out.sort((a, b) => a.relFromUnit.localeCompare(b.relFromUnit));
257
+ }
258
+
259
+ /**
260
+ * Collect `export const` binding names.
261
+ *
262
+ * @param source - TypeScript source
263
+ */
264
+ export function listExportedConsts(source: string): string[] {
265
+ const names: string[] = [];
266
+ EXPORT_CONST.lastIndex = 0;
267
+ let match: RegExpExecArray | null;
268
+ while ((match = EXPORT_CONST.exec(source))) {
269
+ const name = match[1];
270
+ if (name) names.push(name);
271
+ }
272
+ return names;
273
+ }
274
+
275
+ /**
276
+ * Emit import + stamped namespace for a tree unit.
277
+ *
278
+ * @param unit - Folder name
279
+ * @param files - Walked route files
280
+ */
281
+ function emitTreeUnit(
282
+ unit: string,
283
+ files: WalkedFlowFile[],
284
+ ): { lines: string[]; catchAll: boolean } {
285
+ const used = new Map<string, string>();
286
+ let catchAll = false;
287
+ const importLines: string[] = [];
288
+ const fieldLines: string[] = [];
289
+
290
+ for (const file of files) {
291
+ if (file.relFromUnit.includes("[...")) catchAll = true;
292
+ assertUnitPrefix(unit, file);
293
+ const spec = importSpecifierFromWalked(`${unit}/${file.relFromUnit}`);
294
+ if (spec.includes("\\")) {
295
+ throw new GenerateAdoptError(
296
+ `Import specifier for ${unit}/${file.relFromUnit} is not POSIX.`,
297
+ );
298
+ }
299
+ const alias = importAlias(unit, file.relFromUnit);
300
+ importLines.push(`import * as ${alias} from ${JSON.stringify(spec)};`);
301
+
302
+ const inferred =
303
+ pathFromFlowFile(`${unit}/${file.relFromUnit}`) ??
304
+ pathFromFlowFile(`src/flows/${unit}/${file.relFromUnit}`);
305
+
306
+ for (const exp of file.exports) {
307
+ const prev = used.get(exp);
308
+ if (prev !== undefined) {
309
+ throw new GenerateAdoptError(
310
+ `Unit "${unit}" exports "${exp}" from both ${prev} and ${file.relFromUnit}.`,
311
+ );
312
+ }
313
+ used.set(exp, file.relFromUnit);
314
+ const flowName = nameFromFlowFile(`${unit}/${file.relFromUnit}`, exp) ?? `${unit}.${exp}`;
315
+ const pathLit = JSON.stringify(inferred ?? "/");
316
+ fieldLines.push(
317
+ ` ${exp}: stampHttpPath(stampFlowName(${alias}.${exp}, ${JSON.stringify(flowName)}), ${pathLit}),`,
318
+ );
319
+ }
320
+ }
321
+
322
+ const lines = [
323
+ ...importLines,
324
+ "",
325
+ `const ${unit} = {`,
326
+ ...fieldLines,
327
+ `};`,
328
+ `export { ${unit} };`,
329
+ ];
330
+ return { lines, catchAll };
331
+ }
332
+
333
+ /**
334
+ * Fail when `flow("other.x")` lives under a different unit folder.
335
+ *
336
+ * @param unit - Folder name
337
+ * @param file - Walked file
338
+ */
339
+ function assertUnitPrefix(unit: string, file: WalkedFlowFile): void {
340
+ EXPLICIT_FLOW_UNIT.lastIndex = 0;
341
+ let match: RegExpExecArray | null;
342
+ while ((match = EXPLICIT_FLOW_UNIT.exec(file.source))) {
343
+ const prefix = match[1];
344
+ if (prefix && prefix !== unit) {
345
+ throw new GenerateAdoptError(
346
+ `flow("${prefix}.…") in ${unit}/${file.relFromUnit} does not match the folder "${unit}".`,
347
+ );
348
+ }
349
+ }
350
+ }
351
+
352
+ /**
353
+ * Stable import alias for a walked file (`notes/[id]/get.ts` → `notes_$id$_get`).
354
+ *
355
+ * @param unit - Unit folder
356
+ * @param relFromUnit - POSIX relative path
357
+ */
358
+ export function importAlias(unit: string, relFromUnit: string): string {
359
+ const posix = toPosixPath(relFromUnit).replace(/\.[^.]+$/, "");
360
+ const body = posix
361
+ .replace(/\[/g, "$")
362
+ .replace(/\]/g, "$")
363
+ .replace(/[^\w$]/g, "_");
364
+ return `${unit}_${body}`;
365
+ }