okengine 0.19.8 → 0.20.0
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/AGENTS.md +1 -1
- package/package.json +1 -1
- package/site/content/docs/client/auth.mdx +1 -2
- package/site/content/docs/client/calling.mdx +8 -8
- package/site/content/docs/client/index.mdx +4 -4
- package/site/content/docs/client/react.mdx +5 -0
- package/site/content/docs/elements/channel/email.mdx +25 -36
- package/site/content/docs/elements/channel/index.mdx +88 -46
- package/site/content/docs/elements/channel/push.mdx +7 -9
- package/site/content/docs/elements/channel/sms.mdx +6 -4
- package/site/content/docs/elements/channel/whatsapp.mdx +11 -9
- package/site/content/docs/elements/clock/index.mdx +14 -25
- package/site/content/docs/elements/flow/http.mdx +24 -8
- package/site/content/docs/elements/flow/index.mdx +15 -12
- package/site/content/docs/elements/flow/routing.mdx +166 -122
- package/site/content/docs/elements/gate/rls.mdx +2 -2
- package/site/content/docs/elements/store/index.mdx +11 -3
- package/site/content/docs/elements/store/search.mdx +5 -5
- package/site/content/docs/elements/store/sql.mdx +91 -33
- package/site/content/docs/elements/vault/index.mdx +3 -5
- package/site/content/docs/plugins/magic-link.mdx +4 -3
- package/site/content/docs/plugins/otp.mdx +4 -3
- package/site/content/docs/plugins/two-factor.mdx +4 -0
- package/site/content/docs/providers/index.mdx +1 -1
- package/site/content/docs/recipes/index.mdx +1 -1
- package/site/content/docs/reference/cli.mdx +13 -4
- package/site/content/docs/reference/configuration.mdx +7 -7
- package/site/content/docs/reference/errors.mdx +30 -30
- package/site/content/docs/reference/fx.mdx +16 -8
- package/site/content/docs/reference/i18n.mdx +4 -4
- package/site/content/docs/reference/plugins.mdx +4 -4
- package/site/content/docs/understand/try-it.mdx +2 -0
- package/src/cli/ai-setup/ai-setup.test.ts +40 -0
- package/src/cli/ai-setup/apply.ts +28 -46
- package/src/cli/ask-vault-gaps.test.ts +60 -4
- package/src/cli/ask-vault-gaps.ts +59 -3
- package/src/cli/build.test.ts +3 -3
- package/src/cli/build.ts +5 -5
- package/src/cli/db-auto-push.test.ts +11 -0
- package/src/cli/db-auto-push.ts +6 -2
- package/src/cli/db.test.ts +1 -1
- package/src/cli/db.ts +6 -6
- package/src/cli/dev-app-runner.ts +12 -4
- package/src/cli/dev-db-push.test.ts +6 -2
- package/src/cli/dev-schema-sync.ts +1 -1
- package/src/cli/dev.test.ts +10 -7
- package/src/cli/dev.ts +18 -12
- package/src/cli/ensure-drizzle-config.ts +4 -3
- package/src/client-react/browser.test.ts +23 -0
- package/src/client-react/use-live-query.ts +1 -1
- package/src/compiler/flow-path.test.ts +1 -0
- package/src/compiler/flow-path.ts +1 -1
- package/src/compiler/generate-adopt.test.ts +55 -1
- package/src/compiler/generate-adopt.ts +111 -21
- package/src/config/index.ts +6 -4
- package/src/console/ui-next/dist/assets/{access-page-C5yG4aS2.js → access-page-DFLu0wTA.js} +1 -1
- package/src/console/ui-next/dist/assets/{agent-disclosure-D2ToVK86.js → agent-disclosure-DGscxaF5.js} +1 -1
- package/src/console/ui-next/dist/assets/{cache-glyph-7HUR0kCz.js → cache-glyph-BGmRZk7d.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-D0ky3aXt.js → call-pii-button--feUYxvG.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-BLUHH2dB.js → collapsible-JWvpaiGY.js} +1 -1
- package/src/console/ui-next/dist/assets/{duration-tone-BgHEFMtm.js → duration-tone-D9yCJG4n.js} +1 -1
- package/src/console/ui-next/dist/assets/{flows-page-DDybqshQ.js → flows-page-Bs6MD9GB.js} +1 -1
- package/src/console/ui-next/dist/assets/{highlighted-json-BFiJKYV4.js → highlighted-json-xH8MrEnv.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-BVAcB0bI.js → http-method-C4vB6ZIw.js} +1 -1
- package/src/console/ui-next/dist/assets/{index-BID6LSYI.js → index-yTCY4AcS.js} +3 -3
- package/src/console/ui-next/dist/assets/{observability-page-CANo_mMK.js → observability-page-BxJ3R6dU.js} +1 -1
- package/src/console/ui-next/dist/assets/{replica-lag-CqQGCshp.js → replica-lag-QRKB_IE8.js} +1 -1
- package/src/console/ui-next/dist/assets/{request-meta-DyUDkEt4.js → request-meta-DqZ-fMu5.js} +1 -1
- package/src/console/ui-next/dist/assets/{store-page-CA0rJ_Ow.js → store-page-Dixb6L7a.js} +1 -1
- package/src/console/ui-next/dist/assets/{trace-detail-sheet-DjH_fMzI.js → trace-detail-sheet-CazhjtiU.js} +1 -1
- package/src/console/ui-next/dist/assets/{tree-expand-toggle-CWSomIeQ.js → tree-expand-toggle-DlnqYKfr.js} +1 -1
- package/src/console/ui-next/dist/assets/{units-page-Cz9TIViw.js → units-page-BXTLjU2-.js} +1 -1
- package/src/console/ui-next/dist/assets/{vault-page-Bb1o1O9p.js → vault-page-39KR__bc.js} +1 -1
- package/src/console/ui-next/dist/index.html +1 -1
- package/src/drivers/clock-postgres.test.ts +10 -2
- package/src/drivers/clock-postgres.ts +18 -2
- package/src/drivers/vault-driver-removal.test.ts +2 -2
- package/src/elements/channel/declare.ts +66 -3
- package/src/elements/channel/runtime.ts +9 -11
- package/src/elements/channel.test.ts +42 -0
- package/src/elements/channel.ts +4 -2
- package/src/elements/clock/reconcile.ts +45 -24
- package/src/elements/clock.test.ts +33 -0
- package/src/elements/store/emit-drizzle.ts +285 -65
- package/src/elements/store/load-plugin-tables.ts +1 -1
- package/src/elements/store/prepare-row.test.ts +57 -4
- package/src/elements/store/schema-decl.test.ts +178 -0
- package/src/elements/store/sql-session.ts +44 -2
- package/src/elements/store/table.ts +8 -6
- package/src/kernel/adopt-barrel-fresh.test.ts +1 -1
- package/src/kernel/app.ts +26 -32
- package/src/kernel/auto-registry.test.ts +26 -1
- package/src/kernel/boot.ts +2 -2
- package/src/kernel/boundary-contract.ts +6 -1
- package/src/kernel/errors.ts +3 -3
- package/src/kernel/flow-units.ts +3 -3
- package/src/kernel/fx.ts +12 -2
- package/src/kernel/mutation-id.ts +8 -0
- package/src/kernel/plugin.ts +4 -3
- package/src/kernel/project-out.test.ts +176 -0
- package/src/kernel/project-out.ts +91 -0
- package/src/kernel/realtime-bind.ts +2 -3
- package/src/kernel/router/linear.ts +12 -6
- package/src/kernel/router.test.ts +13 -0
- package/src/plugins/magic-link.ts +25 -24
- package/src/plugins/otp.ts +35 -24
- package/src/plugins/two-factor.ts +15 -0
- package/src/runs/duckdb.test.ts +2 -2
|
@@ -54,7 +54,7 @@ export const app = oke({ name: "shop", env: "dev" }).plug(audit);
|
|
|
54
54
|
<Step>
|
|
55
55
|
### Everything derives as usual
|
|
56
56
|
|
|
57
|
-
Plugin flows appear in the Manifest, plugin tables land in `
|
|
57
|
+
Plugin flows appear in the Manifest, plugin tables land in `src/db/drizzle/` on the next `oke db push`, plugin panels show up in the Console. No extra wiring — a contribution is ordinary OKE, just authored elsewhere.
|
|
58
58
|
|
|
59
59
|
</Step>
|
|
60
60
|
|
|
@@ -76,8 +76,8 @@ Every method below exists on both the fluent definition and the boot-time builde
|
|
|
76
76
|
| `.clock(decl)` | A named clock schedule — merged into boot clocks |
|
|
77
77
|
| `.signal(decl)` | A signal declaration — merged into boot signals |
|
|
78
78
|
| `.gate(decl)` | A gate declaration — merged into boot gates |
|
|
79
|
-
| `.channelTemplate(decl)` |
|
|
80
|
-
| `.channelCatalog(catalog)` |
|
|
79
|
+
| `.channelTemplate(decl)` | `channel.<medium>().template(…)` — `catalog` on the decl drains into boot |
|
|
80
|
+
| `.channelCatalog(catalog)` | Overlay bodies after `.template({ catalog })` (`{{field}}`); later locale wins |
|
|
81
81
|
| `.driver(id, impl)` | A protocol-named driver for an existing element |
|
|
82
82
|
| `.image(role, recipe)` | An image recipe for a docker role |
|
|
83
83
|
| `.table(name, columns, options)` | A whole DB table, merged into the generated schema (`options.description` / `plane` optional) |
|
|
@@ -226,7 +226,7 @@ Extending an existing **app-owned** table with plugin columns is not supported i
|
|
|
226
226
|
two configurations of one plugin would silently diverge. Pass identical config, or rename one
|
|
227
227
|
instance.
|
|
228
228
|
</Accordion>
|
|
229
|
-
<Accordion title="My plugin table is missing from
|
|
229
|
+
<Accordion title="My plugin table is missing from drizzle/">
|
|
230
230
|
The CLI reads table contributions from the live app entry. Make sure the plugin is actually
|
|
231
231
|
`.plug()`ed in `src/app.ts` (or `db.entry` if overridden), then re-run `oke db push`.
|
|
232
232
|
</Accordion>
|
|
@@ -21,6 +21,8 @@ cd my-app
|
|
|
21
21
|
bun run dev
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
Default is `-t blank` (health + Store/Vault). `-t shorter` is a URL shortener (`POST /links`, `GET /:code`).
|
|
25
|
+
|
|
24
26
|
Open the Console at the address printed in the terminal and claim it with the code shown there. The Flows listed weren't configured anywhere — they were derived from the code you just scaffolded.
|
|
25
27
|
|
|
26
28
|
## Call it
|
|
@@ -464,6 +464,46 @@ export default defineConfig({
|
|
|
464
464
|
}
|
|
465
465
|
});
|
|
466
466
|
|
|
467
|
+
test("applyAiSetup writes src/core/ai.ts when the barrel is src/core/index.ts", () => {
|
|
468
|
+
const dir = mkdtempSync(join(tmpdir(), "oke-ai-setup-index-"));
|
|
469
|
+
try {
|
|
470
|
+
writeFileSync(
|
|
471
|
+
join(dir, "oke.config.ts"),
|
|
472
|
+
`import { defineConfig } from "okengine/config";
|
|
473
|
+
export default defineConfig({
|
|
474
|
+
drivers: {
|
|
475
|
+
channel: { email: { dev: "console", test: "console", prod: "console" } },
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
`,
|
|
479
|
+
"utf8",
|
|
480
|
+
);
|
|
481
|
+
mkdirSync(join(dir, "src", "core"), { recursive: true });
|
|
482
|
+
writeFileSync(join(dir, "src", "core", "index.ts"), `export * from "./store.ts";\n`, "utf8");
|
|
483
|
+
writeFileSync(
|
|
484
|
+
join(dir, "src", "app.ts"),
|
|
485
|
+
`import "@/core";\nexport const app = {};\n`,
|
|
486
|
+
"utf8",
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
applyAiSetup(dir, {
|
|
490
|
+
driver: "openai-compatible",
|
|
491
|
+
provider: "openrouter",
|
|
492
|
+
chatModel: "openrouter/free",
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
const index = readFileSync(join(dir, "src", "core", "index.ts"), "utf8");
|
|
496
|
+
expect(index).not.toContain("ai.model");
|
|
497
|
+
expect(index).toContain('export * from "./ai.ts"');
|
|
498
|
+
const aiTs = readFileSync(join(dir, "src", "core", "ai.ts"), "utf8");
|
|
499
|
+
expect(aiTs).toContain("smart");
|
|
500
|
+
const app = readFileSync(join(dir, "src", "app.ts"), "utf8");
|
|
501
|
+
expect(app).not.toContain("./core/ai");
|
|
502
|
+
} finally {
|
|
503
|
+
rmSync(dir, { recursive: true, force: true });
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
|
|
467
507
|
test("applyAiSetup writes API token to .env.local and vault.secret into core", () => {
|
|
468
508
|
const dir = mkdtempSync(join(tmpdir(), "oke-ai-setup-key-"));
|
|
469
509
|
try {
|
|
@@ -434,8 +434,8 @@ export function ensureAiApiKeyVaultSecret(cwd: string, apiKeyEnv: string): void
|
|
|
434
434
|
|
|
435
435
|
/**
|
|
436
436
|
* Write AI model declarations into `src/core/ai.ts` when that split exists
|
|
437
|
-
* (so a thin `src/core.ts` barrel stays a re-export),
|
|
438
|
-
* else
|
|
437
|
+
* (so a thin `src/core.ts` / `src/core/index.ts` barrel stays a re-export),
|
|
438
|
+
* else `src/core.ts`.
|
|
439
439
|
*
|
|
440
440
|
* @param cwd - Project root
|
|
441
441
|
* @param input - Setup choices
|
|
@@ -445,25 +445,23 @@ function writeAiModels(cwd: string, input: AiSetupApplyInput): string {
|
|
|
445
445
|
const rendered = renderAiTs(input);
|
|
446
446
|
const coreAiPath = join(cwd, "src", "core", "ai.ts");
|
|
447
447
|
const coreTsPath = join(cwd, "src", "core.ts");
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
448
|
+
const coreIndexPath = join(cwd, "src", "core", "index.ts");
|
|
449
|
+
const splitLayout =
|
|
450
|
+
existsSync(coreAiPath) || (!existsSync(coreTsPath) && existsSync(coreIndexPath));
|
|
451
|
+
|
|
452
|
+
if (splitLayout) {
|
|
453
|
+
mkdirSync(dirname(coreAiPath), { recursive: true });
|
|
454
|
+
const existing = existsSync(coreAiPath) ? readFileSync(coreAiPath, "utf8") : "";
|
|
455
|
+
writeFileSync(
|
|
456
|
+
coreAiPath,
|
|
457
|
+
existing ? resolveAiCoreSource(existing, rendered) : rendered,
|
|
458
|
+
"utf8",
|
|
459
|
+
);
|
|
453
460
|
ensureCoreBarrelExportsAi(cwd);
|
|
454
461
|
ensureCoreImported(cwd);
|
|
455
462
|
return coreAiPath;
|
|
456
463
|
}
|
|
457
464
|
|
|
458
|
-
// Folder layout still in the wild — keep writing a sidecar.
|
|
459
|
-
if (!existsSync(coreTsPath) && existsSync(legacyIndex)) {
|
|
460
|
-
const aiTsPath = join(cwd, "src", "core", "ai.ts");
|
|
461
|
-
mkdirSync(dirname(aiTsPath), { recursive: true });
|
|
462
|
-
writeFileSync(aiTsPath, rendered, "utf8");
|
|
463
|
-
ensureLegacyAiImported(cwd);
|
|
464
|
-
return aiTsPath;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
465
|
mkdirSync(dirname(coreTsPath), { recursive: true });
|
|
468
466
|
if (existsSync(coreTsPath)) {
|
|
469
467
|
const existing = readFileSync(coreTsPath, "utf8");
|
|
@@ -625,16 +623,25 @@ export function ensureNamedOkengineImport(source: string, name: string): string
|
|
|
625
623
|
}
|
|
626
624
|
|
|
627
625
|
/**
|
|
628
|
-
* Keep a
|
|
626
|
+
* Keep a core barrel re-exporting the AI sidecar after a split write.
|
|
627
|
+
*
|
|
628
|
+
* `src/core.ts` → `./core/ai.ts`; `src/core/index.ts` → `./ai.ts`.
|
|
629
629
|
*
|
|
630
630
|
* @param cwd - Project root
|
|
631
631
|
*/
|
|
632
632
|
function ensureCoreBarrelExportsAi(cwd: string): void {
|
|
633
633
|
const coreTsPath = join(cwd, "src", "core.ts");
|
|
634
|
-
if (
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
634
|
+
if (existsSync(coreTsPath)) {
|
|
635
|
+
const src = readFileSync(coreTsPath, "utf8");
|
|
636
|
+
if (/from\s+["']\.\/core\/ai(?:\.ts)?["']/.test(src)) return;
|
|
637
|
+
writeFileSync(coreTsPath, `${src.trimEnd()}\nexport * from "./core/ai.ts";\n`, "utf8");
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const coreIndexPath = join(cwd, "src", "core", "index.ts");
|
|
641
|
+
if (!existsSync(coreIndexPath)) return;
|
|
642
|
+
const src = readFileSync(coreIndexPath, "utf8");
|
|
643
|
+
if (/from\s+["']\.\/ai(?:\.ts)?["']/.test(src)) return;
|
|
644
|
+
writeFileSync(coreIndexPath, `${src.trimEnd()}\nexport * from "./ai.ts";\n`, "utf8");
|
|
638
645
|
}
|
|
639
646
|
|
|
640
647
|
/**
|
|
@@ -656,28 +663,3 @@ function ensureCoreImported(cwd: string): void {
|
|
|
656
663
|
}
|
|
657
664
|
writeFileSync(appPath, `import "@/core";\n${src}`, "utf8");
|
|
658
665
|
}
|
|
659
|
-
|
|
660
|
-
/**
|
|
661
|
-
* Legacy `src/core/*` layout — side-effect import the AI sidecar.
|
|
662
|
-
*
|
|
663
|
-
* @param cwd - Project root
|
|
664
|
-
*/
|
|
665
|
-
function ensureLegacyAiImported(cwd: string): void {
|
|
666
|
-
const candidates: ReadonlyArray<{ readonly rel: string; readonly importLine: string }> = [
|
|
667
|
-
{ rel: "src/app.ts", importLine: `import "./core/ai";\n` },
|
|
668
|
-
{ rel: "src/core/index.ts", importLine: `import "./ai";\n` },
|
|
669
|
-
];
|
|
670
|
-
for (const { rel, importLine } of candidates) {
|
|
671
|
-
const path = join(cwd, rel);
|
|
672
|
-
if (!existsSync(path)) continue;
|
|
673
|
-
const src = readFileSync(path, "utf8");
|
|
674
|
-
if (
|
|
675
|
-
/from\s+["']\.\/(?:core\/)?ai["']/.test(src) ||
|
|
676
|
-
/import\s+["']\.\/(?:core\/)?ai["']/.test(src)
|
|
677
|
-
) {
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
writeFileSync(path, `${importLine}${src}`, "utf8");
|
|
681
|
-
return;
|
|
682
|
-
}
|
|
683
|
-
}
|
|
@@ -2,11 +2,21 @@
|
|
|
2
2
|
* Tests for interactive Vault gap fill during `oke dev`.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { describe, expect, test } from "bun:test";
|
|
6
|
-
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
5
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
6
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { tmpdir } from "node:os";
|
|
8
|
-
import { join } from "node:path";
|
|
9
|
-
import {
|
|
8
|
+
import { join, resolve } from "node:path";
|
|
9
|
+
import { resetRequiredEnvNames, resetSecrets } from "../elements/vault/declare.ts";
|
|
10
|
+
import { resetBindings } from "../kernel/on.ts";
|
|
11
|
+
import { maybeAskVaultGaps, probeVaultGaps } from "./ask-vault-gaps.ts";
|
|
12
|
+
|
|
13
|
+
const OKE = resolve(import.meta.dir, "../index.ts");
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
resetBindings();
|
|
17
|
+
resetSecrets();
|
|
18
|
+
resetRequiredEnvNames();
|
|
19
|
+
});
|
|
10
20
|
|
|
11
21
|
describe("maybeAskVaultGaps", () => {
|
|
12
22
|
test("skips when non-TTY", async () => {
|
|
@@ -82,3 +92,49 @@ describe("maybeAskVaultGaps", () => {
|
|
|
82
92
|
}
|
|
83
93
|
});
|
|
84
94
|
});
|
|
95
|
+
|
|
96
|
+
describe("probeVaultGaps", () => {
|
|
97
|
+
test("sees secrets on oke({ secrets }) after consume drains the registry", async () => {
|
|
98
|
+
const dir = mkdtempSync(join(tmpdir(), "probe-vault-"));
|
|
99
|
+
const name = `PROBE_GAP_${crypto.randomUUID().replaceAll("-", "_")}`;
|
|
100
|
+
try {
|
|
101
|
+
writeFileSync(
|
|
102
|
+
join(dir, "app.ts"),
|
|
103
|
+
`
|
|
104
|
+
import { oke, vault } from ${JSON.stringify(OKE)};
|
|
105
|
+
export const key = vault.secret(${JSON.stringify(name)}, {
|
|
106
|
+
description: "probe gap",
|
|
107
|
+
});
|
|
108
|
+
export const app = oke({
|
|
109
|
+
name: "probe-vault",
|
|
110
|
+
autoBoot: false,
|
|
111
|
+
secrets: [key],
|
|
112
|
+
});
|
|
113
|
+
`,
|
|
114
|
+
);
|
|
115
|
+
const gaps = await probeVaultGaps(dir, "app.ts");
|
|
116
|
+
expect(gaps.map((g) => g.name)).toContain(name);
|
|
117
|
+
} finally {
|
|
118
|
+
rmSync(dir, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("sees auto-registered vault.secret after oke() consume", async () => {
|
|
123
|
+
const dir = mkdtempSync(join(tmpdir(), "probe-vault-auto-"));
|
|
124
|
+
const name = `PROBE_AUTO_${crypto.randomUUID().replaceAll("-", "_")}`;
|
|
125
|
+
try {
|
|
126
|
+
writeFileSync(
|
|
127
|
+
join(dir, "app.ts"),
|
|
128
|
+
`
|
|
129
|
+
import { oke, vault } from ${JSON.stringify(OKE)};
|
|
130
|
+
vault.secret(${JSON.stringify(name)}, { description: "auto registry gap" });
|
|
131
|
+
export const app = oke({ name: "probe-vault-auto", autoBoot: false });
|
|
132
|
+
`,
|
|
133
|
+
);
|
|
134
|
+
const gaps = await probeVaultGaps(dir, "app.ts");
|
|
135
|
+
expect(gaps.map((g) => g.name)).toContain(name);
|
|
136
|
+
} finally {
|
|
137
|
+
rmSync(dir, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -9,8 +9,10 @@ import { resolve } from "node:path";
|
|
|
9
9
|
import { resolveDriverId } from "../config/index.ts";
|
|
10
10
|
import { VAULT_DEFAULTS } from "../config/driver-defaults.ts";
|
|
11
11
|
import { resolveAppEntryForPluginTables } from "../elements/store/load-plugin-tables.ts";
|
|
12
|
+
import type { VaultSecretDecl } from "../elements/vault/declare.ts";
|
|
12
13
|
import { buildVaultBootChain, normalizeVaultDriverId } from "../elements/vault/boot-chain.ts";
|
|
13
14
|
import { createVaultRuntime, VaultBootError, type VaultGap } from "../elements/vault/runtime.ts";
|
|
15
|
+
import type { OkeOptions } from "../kernel/app.ts";
|
|
14
16
|
import { requiredEnvRegistry, secretRegistry } from "../kernel/element-registries.ts";
|
|
15
17
|
import { formatCliChrome } from "../term.ts";
|
|
16
18
|
import { loadOkeConfig } from "./load-config.ts";
|
|
@@ -43,6 +45,10 @@ export interface AskVaultGapsOptions {
|
|
|
43
45
|
/**
|
|
44
46
|
* Probe Vault contracts declared by the app entry and return boot gaps.
|
|
45
47
|
*
|
|
48
|
+
* `oke()` drains `secretRegistry` at construction (`registry: "consume"`),
|
|
49
|
+
* so the probe reads `app.$options.secrets` — the same list boot uses —
|
|
50
|
+
* and unions any leftovers still on the module-evaluation registries.
|
|
51
|
+
*
|
|
46
52
|
* @param cwd - Project root
|
|
47
53
|
* @param entry - Optional entry override
|
|
48
54
|
*/
|
|
@@ -54,9 +60,12 @@ export async function probeVaultGaps(cwd: string, entry?: string): Promise<reado
|
|
|
54
60
|
try {
|
|
55
61
|
const entryAbs = await resolveAppEntryForPluginTables(cwd, entry);
|
|
56
62
|
if (!entryAbs) return [];
|
|
57
|
-
await import(entryAbs)
|
|
58
|
-
const
|
|
59
|
-
const
|
|
63
|
+
const mod = (await import(entryAbs)) as Record<string, unknown>;
|
|
64
|
+
const fromApp = vaultProbeContracts(mod);
|
|
65
|
+
const secrets = unionSecrets(fromApp?.secrets ?? [], secretRegistry.slice());
|
|
66
|
+
const requiredEnv = [
|
|
67
|
+
...new Set([...(fromApp?.requiredEnv ?? []), ...requiredEnvRegistry.slice()]),
|
|
68
|
+
];
|
|
60
69
|
if (secrets.length === 0 && requiredEnv.length === 0) return [];
|
|
61
70
|
|
|
62
71
|
const loaded = await loadOkeConfig(cwd).catch(() => null);
|
|
@@ -91,6 +100,53 @@ export async function probeVaultGaps(cwd: string, entry?: string): Promise<reado
|
|
|
91
100
|
}
|
|
92
101
|
}
|
|
93
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Contracts the imported `oke()` app will boot with (`$options.secrets` /
|
|
105
|
+
* `vault.requiredEnv`). Undefined when the module has no bootable app.
|
|
106
|
+
*
|
|
107
|
+
* @param mod - Imported entry namespace
|
|
108
|
+
*/
|
|
109
|
+
function vaultProbeContracts(mod: Record<string, unknown>):
|
|
110
|
+
| {
|
|
111
|
+
readonly secrets: readonly VaultSecretDecl[];
|
|
112
|
+
readonly requiredEnv: readonly string[];
|
|
113
|
+
}
|
|
114
|
+
| undefined {
|
|
115
|
+
for (const key of ["app", "default", ...Object.keys(mod)]) {
|
|
116
|
+
const value = mod[key];
|
|
117
|
+
if (!value || typeof value !== "object") continue;
|
|
118
|
+
const boot = (value as { boot?: unknown }).boot;
|
|
119
|
+
const options = (value as { $options?: OkeOptions }).$options;
|
|
120
|
+
if (typeof boot !== "function" || options === undefined) continue;
|
|
121
|
+
return {
|
|
122
|
+
secrets: options.secrets ?? [],
|
|
123
|
+
requiredEnv: options.vault?.requiredEnv ?? [],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Concat secret lists, skipping later entries that share a contract name.
|
|
131
|
+
*
|
|
132
|
+
* @param primary - App `$options.secrets` (boot source of truth)
|
|
133
|
+
* @param extra - Registry leftovers (`registry: "ignore"` / no `oke()` export)
|
|
134
|
+
*/
|
|
135
|
+
function unionSecrets(
|
|
136
|
+
primary: readonly VaultSecretDecl[],
|
|
137
|
+
extra: readonly VaultSecretDecl[],
|
|
138
|
+
): VaultSecretDecl[] {
|
|
139
|
+
const out: VaultSecretDecl[] = [...primary];
|
|
140
|
+
const names = new Set(primary.map((secret) => secret.name));
|
|
141
|
+
for (const secret of extra) {
|
|
142
|
+
if (!names.has(secret.name)) {
|
|
143
|
+
names.add(secret.name);
|
|
144
|
+
out.push(secret);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
94
150
|
/**
|
|
95
151
|
* After compose hydrate (and usually schema push), fill Vault boot gaps
|
|
96
152
|
* one-by-one into `.env.local`. Skips when non-TTY or no gaps.
|
package/src/cli/build.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `oke build` regenerates `src/flows/
|
|
2
|
+
* `oke build` regenerates `src/flows/index.ts` before bundling — real
|
|
3
3
|
* before/after proof, not an assumption about the wiring.
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -16,13 +16,13 @@ async function makeUnit(root: string, name: string): Promise<void> {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
describe("oke build — .adopt() barrel regeneration", () => {
|
|
19
|
-
test("real before/after: no
|
|
19
|
+
test("real before/after: no index.ts before build, real barrel content after", async () => {
|
|
20
20
|
const root = await mkdtemp(join(tmpdir(), "oke-build-adopt-"));
|
|
21
21
|
try {
|
|
22
22
|
await makeUnit(root, "main");
|
|
23
23
|
await makeUnit(root, "notes");
|
|
24
24
|
|
|
25
|
-
const generatedPath = join(root, "src/flows/
|
|
25
|
+
const generatedPath = join(root, "src/flows/index.ts");
|
|
26
26
|
await expect(readFile(generatedPath, "utf8")).rejects.toThrow();
|
|
27
27
|
|
|
28
28
|
const code = await runBuild({
|
package/src/cli/build.ts
CHANGED
|
@@ -18,16 +18,16 @@ export interface BuildOptions {
|
|
|
18
18
|
}) => Promise<{ success: boolean; logs: string }>;
|
|
19
19
|
/**
|
|
20
20
|
* Inject `.adopt()` barrel regeneration (tests). Default: real
|
|
21
|
-
* `generateAdoptBarrel` + atomic write to `<rootDir>/src/flows/
|
|
22
|
-
* (`
|
|
21
|
+
* `generateAdoptBarrel` + atomic write to `<rootDir>/src/flows/index.ts`
|
|
22
|
+
* (`index.ts.tmp` → rename). Returns the unit names written, for the
|
|
23
23
|
* status line.
|
|
24
24
|
*/
|
|
25
25
|
readonly syncAdoptBarrel?: (rootDir: string) => Promise<readonly string[]>;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* Regenerate `src/flows/
|
|
30
|
-
*
|
|
29
|
+
* Regenerate `src/flows/index.ts` from every `src/flows/<unit>/` folder —
|
|
30
|
+
* pre-step for `oke build` (mirrors `oke db`'s `drizzle/`
|
|
31
31
|
* pre-step). A real file on disk, not a virtual module: identical resolution
|
|
32
32
|
* under `oke dev`'s runtime `import()` and `oke build`'s `Bun.build()`
|
|
33
33
|
* (investigated — a virtual-module Bun plugin does not resolve consistently
|
|
@@ -76,7 +76,7 @@ export async function runBuild(options: BuildOptions = {}): Promise<number> {
|
|
|
76
76
|
try {
|
|
77
77
|
const units = await syncAdoptBarrel(rootDir);
|
|
78
78
|
write(
|
|
79
|
-
`oke build: .adopt() barrel → src/flows/
|
|
79
|
+
`oke build: .adopt() barrel → src/flows/index.ts (${units.length} unit${units.length === 1 ? "" : "s"})\n`,
|
|
80
80
|
);
|
|
81
81
|
const result = await bundle({ entry, outdir, target: bunTarget });
|
|
82
82
|
if (!result.success) {
|
|
@@ -17,7 +17,18 @@ describe("isDomainSchemaWatchPath", () => {
|
|
|
17
17
|
expect(isDomainSchemaWatchPath("src/schema.decl.ts")).toBe(true);
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
+
test("matches split declare modules under schema/", () => {
|
|
21
|
+
expect(isDomainSchemaWatchPath("db/schema/links.ts")).toBe(true);
|
|
22
|
+
expect(isDomainSchemaWatchPath("src/db/schema/daily.ts")).toBe(true);
|
|
23
|
+
expect(isDomainSchemaWatchPath("src/db/schema/index.ts")).toBe(true);
|
|
24
|
+
expect(isDomainSchemaWatchPath("src/db/schema/relations.ts")).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
|
|
20
27
|
test("ignores emit output (avoids push ↔ generated feedback loop)", () => {
|
|
28
|
+
expect(isDomainSchemaWatchPath("src/db/drizzle.ts")).toBe(false);
|
|
29
|
+
expect(isDomainSchemaWatchPath("drizzle.ts")).toBe(false);
|
|
30
|
+
expect(isDomainSchemaWatchPath("src/db/drizzle/index.ts")).toBe(false);
|
|
31
|
+
expect(isDomainSchemaWatchPath("src/db/drizzle/links.ts")).toBe(false);
|
|
21
32
|
expect(isDomainSchemaWatchPath("src/db/schema.drizzle.ts")).toBe(false);
|
|
22
33
|
expect(isDomainSchemaWatchPath("schema.drizzle.ts")).toBe(false);
|
|
23
34
|
expect(isDomainSchemaWatchPath("flows/notes/schema.drizzle.tsx")).toBe(false);
|
package/src/cli/db-auto-push.ts
CHANGED
|
@@ -11,8 +11,8 @@ export const DB_AUTO_PUSH_DEBOUNCE_MS = 300;
|
|
|
11
11
|
* Whether a watched path should trigger `oke db push`.
|
|
12
12
|
*
|
|
13
13
|
* Inputs only: declaration / drizzle config / app entry. Never the emit
|
|
14
|
-
* output (`
|
|
15
|
-
* watching
|
|
14
|
+
* output (`src/db/drizzle/`) — push rewrites those files every run, so
|
|
15
|
+
* watching them loops `oke db push` forever under `oke dev`.
|
|
16
16
|
*
|
|
17
17
|
* @param filename - Relative path from the watcher (may be undefined)
|
|
18
18
|
*/
|
|
@@ -21,8 +21,10 @@ export function isDomainSchemaWatchPath(filename: string | null | undefined): bo
|
|
|
21
21
|
const normalized = filename.replace(/\\/g, "/");
|
|
22
22
|
const base = basename(normalized);
|
|
23
23
|
// Emit output — never a watch trigger (feedback loop with `oke db push`).
|
|
24
|
+
if (base === "drizzle.ts" || base === "drizzle.tsx") return false;
|
|
24
25
|
if (base === "schema.drizzle.ts" || base === "schema.drizzle.tsx") return false;
|
|
25
26
|
if (base === "schema.generated.ts" || base === "schema.generated.tsx") return false;
|
|
27
|
+
if (/(^|\/)drizzle\/.+\.tsx?$/.test(normalized)) return false;
|
|
26
28
|
if (base === "schema.ts" || base === "schema.tsx") return true;
|
|
27
29
|
if (base === "schema.decl.ts" || base === "schema.decl.tsx") return true;
|
|
28
30
|
if (base === "drizzle.config.ts" || base === "drizzle.config.js") return true;
|
|
@@ -30,6 +32,8 @@ export function isDomainSchemaWatchPath(filename: string | null | undefined): bo
|
|
|
30
32
|
if (base === "app.ts" || base === "app.tsx") return true;
|
|
31
33
|
// Nested domain schemas commonly live under src/**/schema.ts (not emit output).
|
|
32
34
|
if (/(^|\/)schema(\.decl)?\.tsx?$/.test(normalized)) return true;
|
|
35
|
+
// Split declare modules: `src/db/schema/links.ts` next to `schema/index.ts`.
|
|
36
|
+
if (/(^|\/)schema\/.+\.tsx?$/.test(normalized)) return true;
|
|
33
37
|
return false;
|
|
34
38
|
}
|
|
35
39
|
|
package/src/cli/db.test.ts
CHANGED
|
@@ -327,7 +327,7 @@ export const app = oke({ name: "plugin-only" }).plug(
|
|
|
327
327
|
const emitted = await emitAbstractSchemaPrestep(dir, loaded.config, () => {}, "test");
|
|
328
328
|
expect(emitted).toBe(true);
|
|
329
329
|
|
|
330
|
-
const generated = await readFile(join(dir, "src", "db", "
|
|
330
|
+
const generated = await readFile(join(dir, "src", "db", "drizzle", "metrics_daily.ts"), "utf8");
|
|
331
331
|
expect(generated).toContain('pgTable("metrics_daily"');
|
|
332
332
|
// Postgres dialect maps abstract integer → bigint (32-bit INTEGER is too narrow).
|
|
333
333
|
expect(generated).toContain('bigint("hits", { mode: "number" })');
|
package/src/cli/db.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* plus explicit seed runs.
|
|
4
4
|
*
|
|
5
5
|
* Distinct from `oke schema generate` (core/plugin stub tables).
|
|
6
|
-
* When abstract decls exist (`src/db/schema.
|
|
7
|
-
* not a third schema CLI.
|
|
6
|
+
* When abstract decls exist (`src/db/schema.ts` or `src/db/schema/index.ts`),
|
|
7
|
+
* emits Drizzle first — not a third schema CLI.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { resolve } from "node:path";
|
|
@@ -279,7 +279,7 @@ export async function runDb(sub: DbSubcommand, options: DbOptions = {}): Promise
|
|
|
279
279
|
}
|
|
280
280
|
|
|
281
281
|
/**
|
|
282
|
-
* Emit `
|
|
282
|
+
* Emit `drizzle/` from abstract decls + live plugged plugin tables.
|
|
283
283
|
*
|
|
284
284
|
* @param cwd - Project root
|
|
285
285
|
* @param config - Loaded oke config
|
|
@@ -596,9 +596,9 @@ export async function dbCli(args: readonly string[]): Promise<number> {
|
|
|
596
596
|
console.log(`oke db push|generate|migrate|seed|studio|search-backfill [--config|-c] [--env name] [--force]
|
|
597
597
|
|
|
598
598
|
Domain schema sync via drizzle-kit, plus explicit seed and hybrid-search backfill.
|
|
599
|
-
When src/db/schema.
|
|
600
|
-
|
|
601
|
-
contributions — then runs drizzle-kit.
|
|
599
|
+
When src/db/schema.ts or src/db/schema/index.ts and/or a plugged app
|
|
600
|
+
entry exists, emits src/db/drizzle/ from store.schema.table + live plugin
|
|
601
|
+
.table() contributions — then runs drizzle-kit.
|
|
602
602
|
Hand-written src/schema.ts remains supported (emit skipped if nothing to emit).
|
|
603
603
|
Not the same as \`oke schema generate\` (core/plugin stub tables).
|
|
604
604
|
|
|
@@ -81,10 +81,18 @@ if (
|
|
|
81
81
|
process.exit(1);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
})
|
|
84
|
+
try {
|
|
85
|
+
await mod.app.boot({
|
|
86
|
+
rootDir: process.env["OKE_ROOT_DIR"],
|
|
87
|
+
...(manifest !== undefined ? { manifest } : {}),
|
|
88
|
+
});
|
|
89
|
+
} catch (err) {
|
|
90
|
+
// `bun --hot` keeps the watcher alive on an uncaught throw, so `oke dev`
|
|
91
|
+
// would wait the full ready timeout after VaultBootError. Exit so the
|
|
92
|
+
// parent sees a dead child immediately.
|
|
93
|
+
console.error(err);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
88
96
|
const handle = createBunRuntime().serve(mod.app, {
|
|
89
97
|
port,
|
|
90
98
|
hostname,
|
|
@@ -169,7 +169,7 @@ export const entries = pgTable("entries", {
|
|
|
169
169
|
expect(pushes.length).toBeGreaterThan(before);
|
|
170
170
|
});
|
|
171
171
|
|
|
172
|
-
test("
|
|
172
|
+
test("generated drizzle.ts change does not re-trigger dbPush", async () => {
|
|
173
173
|
const cwd = await scaffoldProject();
|
|
174
174
|
const pushes: string[] = [];
|
|
175
175
|
const watcher = controlledWatcher();
|
|
@@ -221,7 +221,11 @@ export const entries = pgTable("entries", {
|
|
|
221
221
|
const before = pushes.length;
|
|
222
222
|
expect(before).toBeGreaterThanOrEqual(1);
|
|
223
223
|
|
|
224
|
-
// Simulate emit writing
|
|
224
|
+
// Simulate emit writing drizzle.ts after push — must not loop.
|
|
225
|
+
watcher.change("drizzle.ts");
|
|
226
|
+
watcher.change("src/db/drizzle.ts");
|
|
227
|
+
watcher.change("src/db/drizzle/index.ts");
|
|
228
|
+
watcher.change("src/db/drizzle/links.ts");
|
|
225
229
|
watcher.change("schema.drizzle.ts");
|
|
226
230
|
watcher.change("src/db/schema.drizzle.ts");
|
|
227
231
|
watcher.change("schema.generated.ts");
|
|
@@ -25,7 +25,7 @@ import { loadOkeConfig, resolveImages } from "./load-config.ts";
|
|
|
25
25
|
export interface DevSchemaSyncOptions {
|
|
26
26
|
/** Write output (default: stdout). */
|
|
27
27
|
readonly write?: (text: string) => void;
|
|
28
|
-
/** Skip emitting `
|
|
28
|
+
/** Skip emitting `drizzle/` (tests). */
|
|
29
29
|
readonly skipEmit?: boolean;
|
|
30
30
|
/** Injectable push (tests). Default: `runDb("push", ...)`. */
|
|
31
31
|
readonly pushFn?: (cwd: string, env: ConfigEnv) => Promise<number>;
|
package/src/cli/dev.test.ts
CHANGED
|
@@ -706,7 +706,7 @@ describe("oke dev hot reload", () => {
|
|
|
706
706
|
});
|
|
707
707
|
|
|
708
708
|
describe("oke dev syncAdoptBarrel atomic write", () => {
|
|
709
|
-
test("default path regenerates
|
|
709
|
+
test("default path regenerates index.ts and leaves no .tmp behind", async () => {
|
|
710
710
|
const dir = await mkdtemp(join(tmpdir(), "oke-dev-adopt-atomic-"));
|
|
711
711
|
await mkdir(join(dir, "src/flows/notes"), { recursive: true });
|
|
712
712
|
await Bun.write(join(dir, "src/flows/notes/index.ts"), "export {};\n");
|
|
@@ -791,9 +791,12 @@ describe("oke dev syncAdoptBarrel atomic write", () => {
|
|
|
791
791
|
});
|
|
792
792
|
|
|
793
793
|
describe("isFlowsTreeWatchPath", () => {
|
|
794
|
-
test("flows tree files trigger regen;
|
|
794
|
+
test("flows tree files trigger regen; adopt barrel does not", () => {
|
|
795
795
|
expect(isFlowsTreeWatchPath("flows/notes/[id]/get.ts")).toBe(true);
|
|
796
796
|
expect(isFlowsTreeWatchPath("flows\\notes\\list.ts")).toBe(true);
|
|
797
|
+
expect(isFlowsTreeWatchPath("flows/notes/index.ts")).toBe(true);
|
|
798
|
+
expect(isFlowsTreeWatchPath("flows/index.ts")).toBe(false);
|
|
799
|
+
expect(isFlowsTreeWatchPath("flows/index.ts.tmp")).toBe(false);
|
|
797
800
|
expect(isFlowsTreeWatchPath("flows/generated.ts")).toBe(false);
|
|
798
801
|
expect(isFlowsTreeWatchPath("flows/generated.ts.tmp")).toBe(false);
|
|
799
802
|
expect(isFlowsTreeWatchPath("core.ts")).toBe(false);
|
|
@@ -953,7 +956,7 @@ describe("oke dev Docker-first", () => {
|
|
|
953
956
|
});
|
|
954
957
|
});
|
|
955
958
|
|
|
956
|
-
describe("oke dev schema
|
|
959
|
+
describe("oke dev schema declare sync framing", () => {
|
|
957
960
|
let session: DevSession | undefined;
|
|
958
961
|
|
|
959
962
|
afterEach(() => {
|
|
@@ -981,7 +984,7 @@ describe("oke dev schema.decl sync framing", () => {
|
|
|
981
984
|
await Bun.write(join(dir, "src/app.ts"), "export {}\n");
|
|
982
985
|
// Genuine definition error: `.references()` target is undefined → emit throws.
|
|
983
986
|
await Bun.write(
|
|
984
|
-
join(dir, "src/db/schema.
|
|
987
|
+
join(dir, "src/db/schema.ts"),
|
|
985
988
|
`import { store, field } from ${JSON.stringify(OKE_INDEX)};
|
|
986
989
|
|
|
987
990
|
export const authors = store.schema.table("authors", {
|
|
@@ -1024,8 +1027,8 @@ export const posts = store.schema.table("posts", {
|
|
|
1024
1027
|
session = result.session;
|
|
1025
1028
|
|
|
1026
1029
|
const plain = writes.join("").replace(/\x1b\[[0-9;]*m/g, "");
|
|
1027
|
-
expect(plain).toContain("schema.
|
|
1028
|
-
expect(plain).toMatch(/●\s*schema\.
|
|
1030
|
+
expect(plain).toContain("schema.ts has an error");
|
|
1031
|
+
expect(plain).toMatch(/●\s*schema\.ts has an error/);
|
|
1029
1032
|
expect(plain).not.toContain("oke db push (dev) skipped");
|
|
1030
1033
|
}, 60_000);
|
|
1031
1034
|
|
|
@@ -1065,7 +1068,7 @@ export const posts = store.schema.table("posts", {
|
|
|
1065
1068
|
|
|
1066
1069
|
const plain = writes.join("").replace(/\x1b\[[0-9;]*m/g, "");
|
|
1067
1070
|
expect(plain).toContain("oke db push (dev) skipped — docker mode: oke.config.ts not found");
|
|
1068
|
-
expect(plain).not.toContain("schema.
|
|
1071
|
+
expect(plain).not.toContain("schema.ts has an error");
|
|
1069
1072
|
}, 60_000);
|
|
1070
1073
|
});
|
|
1071
1074
|
|