okengine 0.10.1 → 0.10.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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/site/content/docs/elements/clock.mdx +2 -2
- package/site/content/docs/elements/flow.mdx +10 -10
- package/site/content/docs/elements/gate.mdx +4 -4
- package/site/content/docs/elements/signal.mdx +4 -7
- package/site/content/docs/elements/store.mdx +1 -1
- package/site/content/docs/elements/vault.mdx +1 -1
- package/site/content/docs/get-started/basic-usage.mdx +1 -3
- package/site/content/docs/get-started/introduction.mdx +1 -1
- package/site/content/docs/reference/cli.md +12 -12
- package/site/content/docs/reference/fx.mdx +3 -4
- package/src/auth/auth.test.ts +1 -2
- package/src/auth/bindings.ts +5 -15
- package/src/auth/gate-auth.test.ts +2 -2
- package/src/cli/ai-setup/apply.ts +13 -0
- package/src/cli/build.test.ts +67 -0
- package/src/cli/build.ts +34 -0
- package/src/cli/db-seed.ts +1 -1
- package/src/cli/dev-controls.test.ts +43 -48
- package/src/cli/dev-controls.ts +23 -172
- package/src/cli/dev.test.ts +2 -4
- package/src/cli/dev.ts +37 -16
- package/src/client/notes-contract.test.ts +3 -4
- package/src/compiler/aot.test.ts +1 -2
- package/src/compiler/extract.test.ts +76 -21
- package/src/compiler/extract.ts +3 -3
- package/src/compiler/fixtures/raw/raw-unannotated.ts +2 -4
- package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +4 -8
- package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -2
- package/src/compiler/fixtures/skyport/src/flows/support/index.ts +1 -2
- package/src/compiler/fixtures/skyport.expected.json +3 -3
- package/src/compiler/fixtures/triggers/five-triggers.ts +5 -10
- package/src/compiler/generate-adopt.test.ts +85 -0
- package/src/compiler/generate-adopt.ts +85 -0
- package/src/console/server/dry-run.audit.test.ts +2 -2
- package/src/console/server/flows.ts +50 -150
- package/src/docker/compose.ts +13 -3
- package/src/docker/recipes/llama-cpp.ts +10 -1
- package/src/docker/stack-id.ts +1 -0
- package/src/elements/channel/declare.ts +27 -1
- package/src/elements/clock/chaos-child.ts +3 -6
- package/src/elements/clock/chaos.test.ts +1 -2
- package/src/elements/clock.test.ts +2 -4
- package/src/elements/signal/declare.ts +25 -1
- package/src/elements/store/declare.ts +27 -1
- package/src/elements/store/resource.ts +6 -11
- package/src/elements/store/upsert-app.test.ts +1 -2
- package/src/elements/vault/declare.ts +29 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +103 -0
- package/src/kernel/app.ts +66 -9
- package/src/kernel/auto-registry.test.ts +198 -0
- package/src/kernel/boot-bind/honor-config.test.ts +5 -5
- package/src/kernel/boot.test.ts +19 -15
- package/src/kernel/boot.ts +81 -1
- package/src/kernel/call.test.ts +5 -10
- package/src/kernel/compensate.test.ts +3 -6
- package/src/kernel/concurrency.test.ts +3 -5
- package/src/kernel/correlation.test.ts +4 -8
- package/src/kernel/edge.test.ts +1 -1
- package/src/kernel/effects-stamping.test.ts +4 -7
- package/src/kernel/element-registries.ts +27 -0
- package/src/kernel/errors.ts +11 -0
- package/src/kernel/flow.test.ts +8 -15
- package/src/kernel/flow.ts +12 -14
- package/src/kernel/hook-timing.test.ts +2 -2
- package/src/kernel/hooks.test.ts +5 -10
- package/src/kernel/horizontal-child.ts +5 -10
- package/src/kernel/journal-boot.test.ts +2 -4
- package/src/kernel/pipeline.test.ts +6 -12
- package/src/kernel/plugin/capabilities.test.ts +1 -1
- package/src/kernel/plugin/decorate.test.ts +3 -8
- package/src/kernel/plugin/scoping.test.ts +6 -16
- package/src/kernel/plugin-elements.test.ts +1 -1
- package/src/kernel/plugin-needs.test.ts +1 -1
- package/src/kernel/ready.test.ts +1 -2
- package/src/kernel/registry-isolation.test.ts +5 -5
- package/src/kernel/triggers.ts +1 -1
- package/src/plugins/anonymous.ts +1 -3
- package/src/plugins/compression.test.ts +6 -8
- package/src/plugins/config-source.test.ts +1 -1
- package/src/plugins/config-source.ts +1 -2
- package/src/plugins/cors.test.ts +10 -10
- package/src/plugins/csrf.test.ts +2 -2
- package/src/plugins/headers.test.ts +14 -15
- package/src/plugins/ip-allowlist.test.ts +9 -9
- package/src/plugins/magic-link.ts +2 -6
- package/src/plugins/maintenance-mode.test.ts +6 -6
- package/src/plugins/otp.ts +5 -15
- package/src/plugins/passkey.ts +4 -12
- package/src/plugins/two-factor.ts +3 -9
- package/src/plugins/username.ts +2 -6
- package/src/release/measure.ts +1 -1
- package/src/runs/runs.test.ts +3 -5
- package/src/runtime/serve.test.ts +2 -4
- package/src/test/create-test-app.test.ts +3 -7
- package/src/test/provisions.integration.test.ts +4 -9
- package/src/test/reset-element-registries.ts +31 -0
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -26,7 +26,7 @@ import { on, flow, every } from "okengine";
|
|
|
26
26
|
|
|
27
27
|
export const purgeOld = on(
|
|
28
28
|
every("1h"),
|
|
29
|
-
flow({
|
|
29
|
+
flow("links.purgeOld", {
|
|
30
30
|
do: async (_, fx) => {
|
|
31
31
|
const cutoff = fx.clock.now() - 30 * 24 * 60 * 60 * 1000; // 30 days
|
|
32
32
|
await fx.store(db).delete(links).where(lt(links.createdAt, cutoff));
|
|
@@ -55,7 +55,7 @@ export const dailyReport = clock("daily-report", {
|
|
|
55
55
|
```typescript title="src/flows/reports/daily.ts"
|
|
56
56
|
export const sendDaily = on(
|
|
57
57
|
dailyReport,
|
|
58
|
-
flow({
|
|
58
|
+
flow("reports.sendDaily", {
|
|
59
59
|
do: async (_, fx) => {
|
|
60
60
|
/* … */
|
|
61
61
|
},
|
|
@@ -28,7 +28,7 @@ import { z } from "zod";
|
|
|
28
28
|
|
|
29
29
|
export const createOrder = on(
|
|
30
30
|
http.post("/orders"),
|
|
31
|
-
flow({
|
|
31
|
+
flow("orders.create", {
|
|
32
32
|
in: z.object({ sku: z.string(), qty: z.number().int().min(1) }),
|
|
33
33
|
out: z.object({ id: z.string() }),
|
|
34
34
|
errors: { OutOfStock: z.object({ left: z.number() }) },
|
|
@@ -106,7 +106,7 @@ Built-in codes attach a localized `message` ([Errors](/docs/reference/errors));
|
|
|
106
106
|
```typescript
|
|
107
107
|
export const findOrder = on(
|
|
108
108
|
http.get("/orders/:id").gate(member), // gates evaluate before do runs
|
|
109
|
-
flow({
|
|
109
|
+
flow("orders.find", {
|
|
110
110
|
in: z.object({ id: z.string() }),
|
|
111
111
|
out: Order,
|
|
112
112
|
errors: { NotFound: z.object({}) },
|
|
@@ -124,7 +124,7 @@ await fx.emit(orderPlaced, { orderId: id }); // inside the producing flow
|
|
|
124
124
|
|
|
125
125
|
on(
|
|
126
126
|
orderPlaced,
|
|
127
|
-
flow({
|
|
127
|
+
flow("orders.sendReceipt", {
|
|
128
128
|
do: async ({ orderId }, fx) => {
|
|
129
129
|
/* … */
|
|
130
130
|
},
|
|
@@ -139,7 +139,7 @@ on(
|
|
|
139
139
|
```typescript
|
|
140
140
|
on(
|
|
141
141
|
every("1h"),
|
|
142
|
-
flow({
|
|
142
|
+
flow("sessions.sweepExpired", {
|
|
143
143
|
do: async (_, fx) => {
|
|
144
144
|
const cutoff = fx.clock.now() - 30 * 24 * 60 * 60 * 1000;
|
|
145
145
|
await fx.store(db).delete(sessions).where(lt(sessions.createdAt, cutoff));
|
|
@@ -155,7 +155,7 @@ CDC is built in; the flow receives `{ before, after }`:
|
|
|
155
155
|
```typescript
|
|
156
156
|
on(
|
|
157
157
|
db.table(orders).changed("status"),
|
|
158
|
-
flow({
|
|
158
|
+
flow("orders.reverify", {
|
|
159
159
|
do: ({ before, after }, fx) => fx.log.info("status", { from: before.status, to: after.status }),
|
|
160
160
|
}),
|
|
161
161
|
);
|
|
@@ -166,7 +166,7 @@ on(
|
|
|
166
166
|
Drop `on()` and it is still a real Flow — contracts, Manifest entry, everything. Other flows call it through `fx.call`:
|
|
167
167
|
|
|
168
168
|
```typescript
|
|
169
|
-
export const getOrder = flow({
|
|
169
|
+
export const getOrder = flow("orders.get", {
|
|
170
170
|
in: OrderRef,
|
|
171
171
|
out: Order,
|
|
172
172
|
do: async ({ id }, fx) => {
|
|
@@ -211,7 +211,7 @@ Everything a flow may touch, on one object:
|
|
|
211
211
|
Set `durable: true` and every `fx` call is journaled. Wrap side effects in `fx.step` and they never re-run on replay:
|
|
212
212
|
|
|
213
213
|
```typescript
|
|
214
|
-
export const chargeOrder = flow({
|
|
214
|
+
export const chargeOrder = flow("payments.chargeOrder", {
|
|
215
215
|
durable: true, // every fx call below is journaled
|
|
216
216
|
in: OrderRef,
|
|
217
217
|
out: z.boolean(),
|
|
@@ -233,14 +233,14 @@ export const chargeOrder = flow({
|
|
|
233
233
|
|
|
234
234
|
Replay is **at-least-once** for a step whose body never persisted (crash mid-step, lease overrun): that step re-runs on reclaim, so keep step bodies short or internally idempotent. A client transport retry starts a _new_ run with a fresh `runId` — request-level idempotency is an application concern, not the journal's.
|
|
235
235
|
|
|
236
|
-
For flaky sub-steps, wrap the work in `fx.retry` **inside** `fx.step` so a completed charge is never retried on resume. Coarse whole-body retry is also available as `flow({ retry: { retries, delay, backoff, jitter } })` and reuses the same journal session.
|
|
236
|
+
For flaky sub-steps, wrap the work in `fx.retry` **inside** `fx.step` so a completed charge is never retried on resume. Coarse whole-body retry is also available as `flow(name, { retry: { retries, delay, backoff, jitter } })` and reuses the same journal session.
|
|
237
237
|
|
|
238
238
|
After a **terminal** failure (`fx.fail` or exhausted retries / uncaught throw), optional
|
|
239
239
|
`compensate` runs under the same journal. Undo work must use **new** step names
|
|
240
240
|
(`undo:…`) — never rewind forward steps:
|
|
241
241
|
|
|
242
242
|
```typescript
|
|
243
|
-
export const chargeOrder = flow({
|
|
243
|
+
export const chargeOrder = flow("payments.chargeOrder", {
|
|
244
244
|
durable: true,
|
|
245
245
|
in: OrderRef,
|
|
246
246
|
out: z.boolean(),
|
|
@@ -295,7 +295,7 @@ Use `fx.all` (or `fx.race`) with thunks. The first rejection aborts siblings thr
|
|
|
295
295
|
</Accordion>
|
|
296
296
|
<Accordion title="Durable flow retried a completed charge">
|
|
297
297
|
|
|
298
|
-
Put `fx.retry` **inside** `fx.step`, not around it. Completed steps replay from the journal and never re-run. `flow({ retry })` is for coarse whole-body retries on the same session.
|
|
298
|
+
Put `fx.retry` **inside** `fx.step`, not around it. Completed steps replay from the journal and never re-run. `flow(name, { retry })` is for coarse whole-body retries on the same session.
|
|
299
299
|
|
|
300
300
|
</Accordion>
|
|
301
301
|
<Accordion title="When does compensate run?">
|
|
@@ -66,7 +66,7 @@ import { member, canBook, fair } from "../../gates";
|
|
|
66
66
|
|
|
67
67
|
export const health = on(
|
|
68
68
|
http.get("/health").gate(gate.public),
|
|
69
|
-
flow({
|
|
69
|
+
flow("health.check", {
|
|
70
70
|
out: HealthOut,
|
|
71
71
|
do: () => ({ ok: true as const }),
|
|
72
72
|
}),
|
|
@@ -74,7 +74,7 @@ export const health = on(
|
|
|
74
74
|
|
|
75
75
|
export const shorten = on(
|
|
76
76
|
http.post("/links").gate(member, canBook, fair),
|
|
77
|
-
flow({
|
|
77
|
+
flow("links.shorten", {
|
|
78
78
|
in: NewLink,
|
|
79
79
|
out: LinkCode,
|
|
80
80
|
errors: { Taken },
|
|
@@ -174,8 +174,8 @@ Every HTTP trigger must carry a gate or `gate.public`. Omitting both fails boot
|
|
|
174
174
|
`GateBootError` — message starts with `gate boot failed —` and lists every gap.
|
|
175
175
|
|
|
176
176
|
```typescript
|
|
177
|
-
on(http.get("/health").gate(gate.public), flow({/* … */}));
|
|
178
|
-
on(http.post("/bookings").gate(member, canBook), flow({/* … */}));
|
|
177
|
+
on(http.get("/health").gate(gate.public), flow("health.check", {/* … */}));
|
|
178
|
+
on(http.post("/bookings").gate(member, canBook), flow("bookings.create", {/* … */}));
|
|
179
179
|
```
|
|
180
180
|
|
|
181
181
|
**Consequence:** first `fetch` / `execute` auto-boots (`autoBoot` defaults to `true`).
|
|
@@ -58,7 +58,7 @@ A signal is a trigger like any other — the consumer is an ordinary Flow:
|
|
|
58
58
|
```typescript title="src/flows/orders/send-confirmation.ts"
|
|
59
59
|
export const sendConfirmation = on(
|
|
60
60
|
orderPlaced,
|
|
61
|
-
flow({
|
|
61
|
+
flow("orders.sendConfirmation", {
|
|
62
62
|
in: z.object({ orderId: z.string(), total: z.number() }),
|
|
63
63
|
do: async (input, fx) => {
|
|
64
64
|
await fx.send(orderConfirmed, { to: "user-1", data: input });
|
|
@@ -131,8 +131,7 @@ export const orderStatus = signal("order-status", {
|
|
|
131
131
|
```typescript title="src/flows/fulfillment/on-order.ts"
|
|
132
132
|
on(
|
|
133
133
|
orderPlaced,
|
|
134
|
-
flow({
|
|
135
|
-
name: "fulfillment.onOrder",
|
|
134
|
+
flow("fulfillment.onOrder", {
|
|
136
135
|
do: async ({ orderId }, fx) => {
|
|
137
136
|
await fx.emit(orderChanged, { orderId, kind: "placed" });
|
|
138
137
|
await fx.emit(orderStatus, { orderId, status: "fulfilling" });
|
|
@@ -147,8 +146,7 @@ on(
|
|
|
147
146
|
```typescript title="src/flows/orders/side-effects.ts"
|
|
148
147
|
on(
|
|
149
148
|
orderChanged,
|
|
150
|
-
flow({
|
|
151
|
-
name: "cache.onOrderChanged",
|
|
149
|
+
flow("cache.onOrderChanged", {
|
|
152
150
|
do: async ({ orderId }, fx) => {
|
|
153
151
|
/* invalidate order:{orderId} */
|
|
154
152
|
},
|
|
@@ -157,8 +155,7 @@ on(
|
|
|
157
155
|
|
|
158
156
|
on(
|
|
159
157
|
orderChanged,
|
|
160
|
-
flow({
|
|
161
|
-
name: "notify.onOrderChanged",
|
|
158
|
+
flow("notify.onOrderChanged", {
|
|
162
159
|
do: async ({ orderId }, fx) => {
|
|
163
160
|
await fx.send(orderConfirmed, { to: "user-1", data: { orderId } });
|
|
164
161
|
},
|
|
@@ -80,7 +80,7 @@ All data access goes through `fx.store(db)` — a typed session, one table at a
|
|
|
80
80
|
```typescript title="src/flows/notes/create.ts"
|
|
81
81
|
export const createNote = on(
|
|
82
82
|
http.post("/notes"),
|
|
83
|
-
flow({
|
|
83
|
+
flow("notes.create", {
|
|
84
84
|
in: z.object({ title: z.string(), body: z.string() }),
|
|
85
85
|
out: z.object({ id: z.string() }),
|
|
86
86
|
do: async (input, fx) => {
|
|
@@ -62,7 +62,7 @@ Call `.reveal()` once at the boundary that needs the real credential:
|
|
|
62
62
|
```typescript title="src/flows/billing/charge.ts"
|
|
63
63
|
export const charge = on(
|
|
64
64
|
http.post("/charge"),
|
|
65
|
-
flow({
|
|
65
|
+
flow("billing.charge", {
|
|
66
66
|
in: ChargeInput,
|
|
67
67
|
out: z.object({ id: z.string() }),
|
|
68
68
|
do: async (input, fx) => {
|
|
@@ -44,9 +44,7 @@ import { z } from "zod";
|
|
|
44
44
|
|
|
45
45
|
export const health = on(
|
|
46
46
|
http.get("/health").gate(gate.public),
|
|
47
|
-
flow({
|
|
48
|
-
name: "main.health",
|
|
49
|
-
unit: "main",
|
|
47
|
+
flow("main.health", {
|
|
50
48
|
out: z.object({ ok: z.literal(true) }),
|
|
51
49
|
do: () => ({ ok: true as const }),
|
|
52
50
|
}),
|
|
@@ -67,18 +67,18 @@ so ● updates live if you stop a container; AI ● tracks model phase while the
|
|
|
67
67
|
AI container is up. Boot does not wait for the model to become ready.
|
|
68
68
|
|
|
69
69
|
On a TTY with `-d`, keyboard controls stay active under Logs. Status ● for
|
|
70
|
-
compose/AI live on the board **above** Logs (not as log lines)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
70
|
+
compose/AI live on the board **above** Logs (not as log lines) — it already
|
|
71
|
+
lists every service, so refresh is enough to see the stack; there is no
|
|
72
|
+
separate services panel. Press `r` to clear the log pane and reprint the
|
|
73
|
+
latest board. `?` also refreshes first so the help panel is not mixed into
|
|
74
|
+
request logs.
|
|
75
|
+
|
|
76
|
+
| Key | Action |
|
|
77
|
+
| --------- | ----------------------------------- |
|
|
78
|
+
| `?` | help |
|
|
79
|
+
| `r` | refresh — clear logs, show latest ● |
|
|
80
|
+
| `q` | quit `oke dev` |
|
|
81
|
+
| `u` / `x` | compose up / stop (whole stack) |
|
|
82
82
|
|
|
83
83
|
Compose stays quiet unless it fails. Process-local boot notices print once under
|
|
84
84
|
the hero (not once per process).
|
|
@@ -7,7 +7,7 @@ icon: "Braces"
|
|
|
7
7
|
`fx` is the second argument of every `do` — the single door to the world. This page is the whole surface; each entry notes the **effect it records**, which is what feeds the Manifest, caching, and capability checks.
|
|
8
8
|
|
|
9
9
|
```typescript
|
|
10
|
-
flow({
|
|
10
|
+
flow("orders.example", {
|
|
11
11
|
do: async (input, fx) => {
|
|
12
12
|
// everything below, on this one object
|
|
13
13
|
},
|
|
@@ -51,8 +51,7 @@ Declare `effects: { reads: ["runs"] }`. Powers native SLO checkers (Clock + Chan
|
|
|
51
51
|
```typescript
|
|
52
52
|
on(
|
|
53
53
|
every("5m"),
|
|
54
|
-
flow({
|
|
55
|
-
name: "ops.slo-check",
|
|
54
|
+
flow("ops.slo-check", {
|
|
56
55
|
effects: { reads: ["runs"], sends: ["slo-alert"] },
|
|
57
56
|
do: async (_, fx) => {
|
|
58
57
|
const breaches = await fx.runs.checkSlo(
|
|
@@ -133,7 +132,7 @@ const rows = await fx.using(
|
|
|
133
132
|
);
|
|
134
133
|
```
|
|
135
134
|
|
|
136
|
-
**Consequence:** put `fx.retry` inside `fx.step` on durable flows so a completed charge never re-runs on resume. Coarse whole-body retry is `flow({ retry: { … } })` on the same journal session.
|
|
135
|
+
**Consequence:** put `fx.retry` inside `fx.step` on durable flows so a completed charge never re-runs on resume. Coarse whole-body retry is `flow(name, { retry: { … } })` on the same journal session.
|
|
137
136
|
|
|
138
137
|
## Channel
|
|
139
138
|
|
package/src/auth/auth.test.ts
CHANGED
|
@@ -177,8 +177,7 @@ describe("cross-plane compile error", () => {
|
|
|
177
177
|
path: "src/flows/console/store.ts",
|
|
178
178
|
flow: "console.store.query",
|
|
179
179
|
source: `
|
|
180
|
-
export const query = flow({
|
|
181
|
-
name: "console.store.query",
|
|
180
|
+
export const query = flow("console.store.query", {
|
|
182
181
|
plane: "operator",
|
|
183
182
|
do: (_i, fx) => fx.auth.userId,
|
|
184
183
|
});
|
package/src/auth/bindings.ts
CHANGED
|
@@ -205,9 +205,7 @@ export function createAuthHttpBindings(
|
|
|
205
205
|
|
|
206
206
|
const base = config.basePath;
|
|
207
207
|
|
|
208
|
-
const refresh = flow({
|
|
209
|
-
name: "auth.refresh",
|
|
210
|
-
unit: "auth",
|
|
208
|
+
const refresh = flow("auth.refresh", {
|
|
211
209
|
plane: "user",
|
|
212
210
|
in: RefreshIn,
|
|
213
211
|
out: SessionTokensOut,
|
|
@@ -231,9 +229,7 @@ export function createAuthHttpBindings(
|
|
|
231
229
|
},
|
|
232
230
|
});
|
|
233
231
|
|
|
234
|
-
const revoke = flow({
|
|
235
|
-
name: "auth.revoke",
|
|
236
|
-
unit: "auth",
|
|
232
|
+
const revoke = flow("auth.revoke", {
|
|
237
233
|
plane: "user",
|
|
238
234
|
in: RevokeIn,
|
|
239
235
|
out: z.object({ ok: z.literal(true) }),
|
|
@@ -252,9 +248,7 @@ export function createAuthHttpBindings(
|
|
|
252
248
|
},
|
|
253
249
|
});
|
|
254
250
|
|
|
255
|
-
const me = flow({
|
|
256
|
-
name: "auth.me",
|
|
257
|
-
unit: "auth",
|
|
251
|
+
const me = flow("auth.me", {
|
|
258
252
|
plane: "user",
|
|
259
253
|
out: MeOut,
|
|
260
254
|
errors: { AuthFailed },
|
|
@@ -289,9 +283,7 @@ export function createAuthHttpBindings(
|
|
|
289
283
|
];
|
|
290
284
|
|
|
291
285
|
if (config.emailAndPassword.enabled) {
|
|
292
|
-
const signInEmail = flow({
|
|
293
|
-
name: "auth.signInEmail",
|
|
294
|
-
unit: "auth",
|
|
286
|
+
const signInEmail = flow("auth.signInEmail", {
|
|
295
287
|
plane: "user",
|
|
296
288
|
in: EmailPasswordIn,
|
|
297
289
|
out: SessionTokensOut,
|
|
@@ -319,9 +311,7 @@ export function createAuthHttpBindings(
|
|
|
319
311
|
},
|
|
320
312
|
});
|
|
321
313
|
|
|
322
|
-
const signUpEmail = flow({
|
|
323
|
-
name: "auth.signUpEmail",
|
|
324
|
-
unit: "auth",
|
|
314
|
+
const signUpEmail = flow("auth.signUpEmail", {
|
|
325
315
|
plane: "user",
|
|
326
316
|
in: EmailPasswordIn,
|
|
327
317
|
out: SessionTokensOut,
|
|
@@ -136,7 +136,7 @@ describe("gate.auth — autoBoot / posture", () => {
|
|
|
136
136
|
});
|
|
137
137
|
|
|
138
138
|
test(".needs(auth) satisfied by gate.auth without .plug(auth())", async () => {
|
|
139
|
-
on(http.get("/x").gate(AUTH_SESSION_GATE), flow(
|
|
139
|
+
on(http.get("/x").gate(AUTH_SESSION_GATE), flow("x", { do: () => ({ ok: true }) }));
|
|
140
140
|
const dependent = plugin("needs-auth", { version: "0.0.1" }).needs("auth");
|
|
141
141
|
const app = oke({
|
|
142
142
|
name: "needs-gate-auth",
|
|
@@ -309,7 +309,7 @@ describe("gate.auth — email Flows + security", () => {
|
|
|
309
309
|
});
|
|
310
310
|
|
|
311
311
|
test("missing gate on custom HTTP still GateBootError", async () => {
|
|
312
|
-
on(http.get("/open"), flow(
|
|
312
|
+
on(http.get("/open"), flow("open", { do: () => ({ ok: true }) }));
|
|
313
313
|
const app = oke({
|
|
314
314
|
name: "gap",
|
|
315
315
|
env: "local",
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import { OLLAMA_IMAGE } from "../../docker/recipes/index.ts";
|
|
8
|
+
import { DEFAULT_DOCKER_DIR } from "../../docker/types.ts";
|
|
8
9
|
|
|
9
10
|
/** Choices applied to the project. */
|
|
10
11
|
export type AiSetupApplyInput = {
|
|
@@ -91,6 +92,18 @@ export function applyAiSetup(
|
|
|
91
92
|
}
|
|
92
93
|
writeFileSync(envPath, env.endsWith("\n") ? env : `${env}\n`, "utf8");
|
|
93
94
|
|
|
95
|
+
// `docker/.env.docker` is regenerated from `.env.local` only on its first
|
|
96
|
+
// boot (existing values there are treated as durable, possibly hand-edited
|
|
97
|
+
// pins — see stack-id.ts). Once it exists, a later `oke ai setup` run must
|
|
98
|
+
// update it directly, or the docker profile keeps the old model forever.
|
|
99
|
+
if (input.chatModel) {
|
|
100
|
+
const dockerEnvPath = join(cwd, DEFAULT_DOCKER_DIR, ".env.docker");
|
|
101
|
+
if (existsSync(dockerEnvPath)) {
|
|
102
|
+
const dockerEnv = readFileSync(dockerEnvPath, "utf8");
|
|
103
|
+
writeFileSync(dockerEnvPath, upsertEnv(dockerEnv, "OKE_AI_MODEL", input.chatModel), "utf8");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
94
107
|
const aiTsPath = join(cwd, "src", "core", "ai.ts");
|
|
95
108
|
mkdirSync(dirname(aiTsPath), { recursive: true });
|
|
96
109
|
writeFileSync(aiTsPath, renderAiTs(input), "utf8");
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `oke build` regenerates `src/flows/generated.ts` before bundling — real
|
|
3
|
+
* before/after proof, not an assumption about the wiring.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from "bun:test";
|
|
7
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
8
|
+
import { tmpdir } from "node:os";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { runBuild } from "./build.ts";
|
|
11
|
+
|
|
12
|
+
async function makeUnit(root: string, name: string): Promise<void> {
|
|
13
|
+
const dir = join(root, "src/flows", name);
|
|
14
|
+
await mkdir(dir, { recursive: true });
|
|
15
|
+
await writeFile(join(dir, "index.ts"), "export const nothing = 1;\n");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("oke build — .adopt() barrel regeneration", () => {
|
|
19
|
+
test("real before/after: no generated.ts before build, real barrel content after", async () => {
|
|
20
|
+
const root = await mkdtemp(join(tmpdir(), "oke-build-adopt-"));
|
|
21
|
+
try {
|
|
22
|
+
await makeUnit(root, "main");
|
|
23
|
+
await makeUnit(root, "notes");
|
|
24
|
+
|
|
25
|
+
const generatedPath = join(root, "src/flows/generated.ts");
|
|
26
|
+
await expect(readFile(generatedPath, "utf8")).rejects.toThrow();
|
|
27
|
+
|
|
28
|
+
const code = await runBuild({
|
|
29
|
+
rootDir: root,
|
|
30
|
+
bundle: async () => ({ success: true, logs: "" }),
|
|
31
|
+
});
|
|
32
|
+
expect(code).toBe(0);
|
|
33
|
+
|
|
34
|
+
const after = await readFile(generatedPath, "utf8");
|
|
35
|
+
expect(after).toContain('export * as main from "./main/index.ts";');
|
|
36
|
+
expect(after).toContain('export * as notes from "./notes/index.ts";');
|
|
37
|
+
} finally {
|
|
38
|
+
await rm(root, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("regeneration runs before bundling, using the injected syncAdoptBarrel", async () => {
|
|
43
|
+
const calls: string[] = [];
|
|
44
|
+
const code = await runBuild({
|
|
45
|
+
rootDir: "/fake/root",
|
|
46
|
+
syncAdoptBarrel: async (rootDir) => {
|
|
47
|
+
calls.push(`sync:${rootDir}`);
|
|
48
|
+
return ["main", "notes"];
|
|
49
|
+
},
|
|
50
|
+
bundle: async () => {
|
|
51
|
+
calls.push("bundle");
|
|
52
|
+
return { success: true, logs: "" };
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
expect(code).toBe(0);
|
|
56
|
+
expect(calls).toEqual(["sync:/fake/root", "bundle"]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("bundle failure still returns non-zero after a successful sync", async () => {
|
|
60
|
+
const code = await runBuild({
|
|
61
|
+
rootDir: "/fake/root",
|
|
62
|
+
syncAdoptBarrel: async () => [],
|
|
63
|
+
bundle: async () => ({ success: false, logs: "boom" }),
|
|
64
|
+
});
|
|
65
|
+
expect(code).toBe(1);
|
|
66
|
+
});
|
|
67
|
+
});
|
package/src/cli/build.ts
CHANGED
|
@@ -2,18 +2,46 @@
|
|
|
2
2
|
* `oke build --target edge` — tree-shaken kernel profile.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { writeFile } from "node:fs/promises";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
|
|
5
8
|
/** Options for {@link runBuild}. */
|
|
6
9
|
export interface BuildOptions {
|
|
7
10
|
readonly target?: "bun" | "node" | "edge";
|
|
8
11
|
readonly entry?: string;
|
|
9
12
|
readonly outdir?: string;
|
|
10
13
|
readonly write?: (text: string) => void;
|
|
14
|
+
/** Project root for the `.adopt()` barrel sync (default `process.cwd()`). */
|
|
15
|
+
readonly rootDir?: string;
|
|
11
16
|
/** Inject bun.build (tests). */
|
|
12
17
|
readonly bundle?: (opts: {
|
|
13
18
|
entry: string;
|
|
14
19
|
outdir: string;
|
|
15
20
|
target: "bun" | "node" | "browser";
|
|
16
21
|
}) => Promise<{ success: boolean; logs: string }>;
|
|
22
|
+
/**
|
|
23
|
+
* Inject `.adopt()` barrel regeneration (tests). Default: real
|
|
24
|
+
* `generateAdoptBarrel` + write to `<rootDir>/src/flows/generated.ts`.
|
|
25
|
+
* Returns the unit names written, for the status line.
|
|
26
|
+
*/
|
|
27
|
+
readonly syncAdoptBarrel?: (rootDir: string) => Promise<readonly string[]>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Regenerate `src/flows/generated.ts` from every `src/flows/<unit>/index.ts`
|
|
32
|
+
* unit folder — pre-step for `oke build` (mirrors `oke db`'s `schema.generated.ts`
|
|
33
|
+
* pre-step). A real file on disk, not a virtual module: identical resolution
|
|
34
|
+
* under `oke dev`'s runtime `import()` and `oke build`'s `Bun.build()`
|
|
35
|
+
* (investigated — a virtual-module Bun plugin does not resolve consistently
|
|
36
|
+
* across those two paths).
|
|
37
|
+
*
|
|
38
|
+
* @param rootDir - Project root
|
|
39
|
+
*/
|
|
40
|
+
async function defaultSyncAdoptBarrel(rootDir: string): Promise<readonly string[]> {
|
|
41
|
+
const { generateAdoptBarrel } = await import("../compiler/generate-adopt.ts");
|
|
42
|
+
const { source, units } = await generateAdoptBarrel({ rootDir });
|
|
43
|
+
await writeFile(join(rootDir, "src/flows/generated.ts"), source);
|
|
44
|
+
return units;
|
|
17
45
|
}
|
|
18
46
|
|
|
19
47
|
/**
|
|
@@ -26,6 +54,7 @@ export async function runBuild(options: BuildOptions = {}): Promise<number> {
|
|
|
26
54
|
const target = options.target ?? "bun";
|
|
27
55
|
const entry = options.entry ?? "src/app.ts";
|
|
28
56
|
const outdir = options.outdir ?? "dist";
|
|
57
|
+
const rootDir = options.rootDir ?? process.cwd();
|
|
29
58
|
const bunTarget = target === "edge" ? "browser" : target === "node" ? "node" : "bun";
|
|
30
59
|
|
|
31
60
|
const bundle =
|
|
@@ -43,8 +72,13 @@ export async function runBuild(options: BuildOptions = {}): Promise<number> {
|
|
|
43
72
|
logs: result.logs.map((l) => String(l)).join("\n"),
|
|
44
73
|
};
|
|
45
74
|
});
|
|
75
|
+
const syncAdoptBarrel = options.syncAdoptBarrel ?? defaultSyncAdoptBarrel;
|
|
46
76
|
|
|
47
77
|
try {
|
|
78
|
+
const units = await syncAdoptBarrel(rootDir);
|
|
79
|
+
write(
|
|
80
|
+
`oke build: .adopt() barrel → src/flows/generated.ts (${units.length} unit${units.length === 1 ? "" : "s"})\n`,
|
|
81
|
+
);
|
|
48
82
|
const result = await bundle({ entry, outdir, target: bunTarget });
|
|
49
83
|
if (!result.success) {
|
|
50
84
|
console.error(result.logs || "oke build: failed");
|
package/src/cli/db-seed.ts
CHANGED
|
@@ -233,7 +233,7 @@ export async function bootSeedFx(
|
|
|
233
233
|
if (!app) {
|
|
234
234
|
throw new Error(`oke db seed: no oke() app export in ${entryAbs}`);
|
|
235
235
|
}
|
|
236
|
-
await app.boot({ env, startScheduler: false, config: config ?? undefined });
|
|
236
|
+
await app.boot({ env, startScheduler: false, config: config ?? undefined, rootDir: cwd });
|
|
237
237
|
const storeRuntime = app.elements?.store ?? app.bootResult?.store;
|
|
238
238
|
if (!storeRuntime) {
|
|
239
239
|
throw new Error(
|