okengine 0.10.2 → 0.10.3
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/package.json +1 -1
- package/site/content/docs/ai/mcp.mdx +0 -1
- package/site/content/docs/elements/ai.mdx +0 -6
- package/site/content/docs/elements/channel.mdx +0 -6
- package/site/content/docs/elements/clock.mdx +0 -6
- package/site/content/docs/elements/flow.mdx +0 -7
- package/site/content/docs/elements/gate.mdx +0 -6
- package/site/content/docs/elements/signal.mdx +0 -6
- package/site/content/docs/elements/store.mdx +8 -15
- package/site/content/docs/elements/vault.mdx +0 -6
- package/site/content/docs/get-started/basic-usage.mdx +0 -6
- package/site/content/docs/get-started/why.mdx +1 -2
- package/site/content/docs/index.mdx +0 -1
- package/site/content/docs/meta.json +0 -1
- package/site/content/docs/reference/fx.mdx +3 -3
- package/src/cli/ai-setup/ai-setup.test.ts +3 -1
- package/src/cli/build.ts +7 -8
- package/src/cli/dev.test.ts +88 -1
- package/src/cli/dev.ts +15 -13
- package/src/cli/doctor.ts +98 -5
- package/src/cli/load-config.images.test.ts +31 -1
- package/src/cli/load-config.ts +2 -2
- package/src/cli/safe-defaults.test.ts +1 -1
- package/src/compiler/extract.ts +12 -0
- package/src/compiler/fixtures/skyport/oke.config.ts +4 -2
- package/src/compiler/generate-adopt.ts +27 -1
- package/src/config/driver-defaults.test.ts +122 -0
- package/src/config/driver-defaults.ts +186 -0
- package/src/config/index.ts +97 -7
- package/src/docker/images-config.test.ts +168 -0
- package/src/kernel/boot-bind/channel.ts +12 -4
- package/src/kernel/boot-bind/clock.ts +3 -3
- package/src/kernel/boot-bind/gate.ts +7 -4
- package/src/kernel/boot-bind/journal.ts +3 -3
- package/src/kernel/boot-bind/signal.ts +3 -1
- package/src/kernel/boot-bind/store.test.ts +19 -0
- package/src/kernel/boot-bind/store.ts +24 -9
- package/src/kernel/boot-bind/vault.ts +3 -3
- package/site/content/docs/console/access.mdx +0 -29
- package/site/content/docs/console/ai.mdx +0 -35
- package/site/content/docs/console/architecture.mdx +0 -35
- package/site/content/docs/console/channels.mdx +0 -37
- package/site/content/docs/console/clock.mdx +0 -51
- package/site/content/docs/console/flows.mdx +0 -31
- package/site/content/docs/console/gates.mdx +0 -73
- package/site/content/docs/console/index.mdx +0 -54
- package/site/content/docs/console/manifest-diff.mdx +0 -34
- package/site/content/docs/console/meta.json +0 -24
- package/site/content/docs/console/overview.mdx +0 -40
- package/site/content/docs/console/plugins.mdx +0 -41
- package/site/content/docs/console/privacy.mdx +0 -32
- package/site/content/docs/console/runs.mdx +0 -50
- package/site/content/docs/console/signals.mdx +0 -40
- package/site/content/docs/console/store.mdx +0 -32
- package/site/content/docs/console/tenancy.mdx +0 -32
- package/site/content/docs/console/traces.mdx +0 -38
- package/site/content/docs/console/vault.mdx +0 -41
|
@@ -3,12 +3,42 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { flattenImagesConfig } from "../config/index.ts";
|
|
6
7
|
import {
|
|
7
8
|
defaultImagesFromConfig,
|
|
8
9
|
resolveImages,
|
|
9
10
|
dockerDevDriverMismatches,
|
|
10
11
|
} from "./load-config.ts";
|
|
11
12
|
|
|
13
|
+
describe("flattenImagesConfig", () => {
|
|
14
|
+
test("flattens store/channel nesting to dotted keys; flat roles pass through", () => {
|
|
15
|
+
expect(
|
|
16
|
+
flattenImagesConfig({
|
|
17
|
+
store: { sql: "postgres:18-alpine", kv: "redis:8-alpine", files: "rustfs/rustfs:1.0.0" },
|
|
18
|
+
channel: { email: "axllent/mailpit:v1.22.3" },
|
|
19
|
+
vault: "openbao/openbao:2.6.1",
|
|
20
|
+
ai: "ollama/ollama:0.32.6",
|
|
21
|
+
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.51",
|
|
22
|
+
proxy: "caddy:2-alpine",
|
|
23
|
+
}),
|
|
24
|
+
).toEqual({
|
|
25
|
+
"store.sql": "postgres:18-alpine",
|
|
26
|
+
"store.kv": "redis:8-alpine",
|
|
27
|
+
"store.files": "rustfs/rustfs:1.0.0",
|
|
28
|
+
"channel.email": "axllent/mailpit:v1.22.3",
|
|
29
|
+
vault: "openbao/openbao:2.6.1",
|
|
30
|
+
ai: "ollama/ollama:0.32.6",
|
|
31
|
+
pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.51",
|
|
32
|
+
proxy: "caddy:2-alpine",
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("undefined / empty input yields empty map", () => {
|
|
37
|
+
expect(flattenImagesConfig(undefined)).toEqual({});
|
|
38
|
+
expect(flattenImagesConfig({})).toEqual({});
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
12
42
|
describe("defaultImagesFromConfig", () => {
|
|
13
43
|
test("maps postgres + redis prod drivers", () => {
|
|
14
44
|
const images = defaultImagesFromConfig({
|
|
@@ -82,7 +112,7 @@ describe("resolveImages", () => {
|
|
|
82
112
|
kv: { prod: "redis" },
|
|
83
113
|
},
|
|
84
114
|
},
|
|
85
|
-
images: {
|
|
115
|
+
images: { store: { sql: "pgvector/pgvector:pg17" } },
|
|
86
116
|
});
|
|
87
117
|
expect(images).toEqual({ "store.sql": "pgvector/pgvector:pg17" });
|
|
88
118
|
});
|
package/src/cli/load-config.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
|
-
import type
|
|
6
|
+
import { flattenImagesConfig, type DriverRef, type OkeConfig } from "../config/index.ts";
|
|
7
7
|
import type { Manifest } from "../manifest/types.ts";
|
|
8
8
|
|
|
9
9
|
/** Default image pins when `images` is omitted but prod drivers need containers. */
|
|
@@ -145,7 +145,7 @@ export function resolveImages(
|
|
|
145
145
|
config?: OkeConfig,
|
|
146
146
|
manifest?: Manifest,
|
|
147
147
|
): Readonly<Record<string, string>> {
|
|
148
|
-
const explicit = config?.images
|
|
148
|
+
const explicit = config?.images ? flattenImagesConfig(config.images) : manifest?.images;
|
|
149
149
|
if (explicit && Object.keys(explicit).length > 0) return explicit;
|
|
150
150
|
if (config) return defaultImagesFromConfig(config);
|
|
151
151
|
return {};
|
|
@@ -16,7 +16,7 @@ describe("oke safe-default overrides", () => {
|
|
|
16
16
|
const dir = await mkdtemp(join(tmpdir(), "oke-safe-docker-"));
|
|
17
17
|
await Bun.write(
|
|
18
18
|
join(dir, "oke.config.ts"),
|
|
19
|
-
`export default { images: {
|
|
19
|
+
`export default { images: { store: { sql: "postgres:18-alpine" } } }\n`,
|
|
20
20
|
);
|
|
21
21
|
// Without --prod, derive still runs but prod overlays are not requested.
|
|
22
22
|
// We assert the parser default by calling runDockerDerive via dockerCli
|
package/src/compiler/extract.ts
CHANGED
|
@@ -1022,8 +1022,20 @@ function collectConfig(opts: AstNode | undefined, scope: ProjectScope): void {
|
|
|
1022
1022
|
const images = objectProp(opts, "images");
|
|
1023
1023
|
if (images) {
|
|
1024
1024
|
const map: Record<string, string> = {};
|
|
1025
|
+
// Nested like `drivers`: `store.*` / `channel.*` flatten to dotted keys;
|
|
1026
|
+
// everything else (`vault`, `ai`, `pgdog`, `proxy`) is already flat.
|
|
1027
|
+
for (const nestKey of ["store", "channel"] as const) {
|
|
1028
|
+
const nested = objectProp(images, nestKey);
|
|
1029
|
+
if (!nested || nested.type !== "ObjectExpression") continue;
|
|
1030
|
+
for (const prop of objectProperties(nested)) {
|
|
1031
|
+
const key = propKey(prop);
|
|
1032
|
+
const val = stringArg((prop as AstNode & { value?: AstNode }).value);
|
|
1033
|
+
if (key && val) map[`${nestKey}.${key}`] = val;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1025
1036
|
for (const prop of objectProperties(images)) {
|
|
1026
1037
|
const key = propKey(prop);
|
|
1038
|
+
if (key === "store" || key === "channel") continue;
|
|
1027
1039
|
const val = stringArg((prop as AstNode & { value?: AstNode }).value);
|
|
1028
1040
|
if (key && val) map[key] = val;
|
|
1029
1041
|
}
|
|
@@ -15,8 +15,10 @@ export default defineConfig({
|
|
|
15
15
|
ai: { prod: { driver: "anthropic" } },
|
|
16
16
|
},
|
|
17
17
|
images: {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
store: {
|
|
19
|
+
sql: "pgvector/pgvector:pg17",
|
|
20
|
+
kv: "valkey/valkey:8-alpine",
|
|
21
|
+
},
|
|
20
22
|
},
|
|
21
23
|
i18n: { locales: ["en", "ar"], default: "ar", dir: { ar: "rtl" } },
|
|
22
24
|
tenancy: { isolation: "row" },
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* `<rootDir>/src/flows/*` has an `index.ts` barrel.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { readdir } from "node:fs/promises";
|
|
15
|
+
import { mkdir, readdir, rename, writeFile } from "node:fs/promises";
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
|
|
18
18
|
/** Options for {@link generateAdoptBarrel}. */
|
|
@@ -37,9 +37,35 @@ const GENERATED_HEADER =
|
|
|
37
37
|
"// AUTO-GENERATED by `oke dev` / `oke build` — do not edit by hand.\n" +
|
|
38
38
|
"// Regenerated from every `src/flows/<unit>/index.ts` unit folder.\n";
|
|
39
39
|
|
|
40
|
+
/** Filename of the on-disk `.adopt()` barrel under the flows directory. */
|
|
41
|
+
export const ADOPT_BARREL_FILE = "generated.ts";
|
|
42
|
+
|
|
40
43
|
/** A folder name usable as an `export * as <name>` binding. */
|
|
41
44
|
const VALID_UNIT_NAME = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
42
45
|
|
|
46
|
+
/**
|
|
47
|
+
* 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.
|
|
51
|
+
*
|
|
52
|
+
* @param rootDir - Project root
|
|
53
|
+
* @param source - Full barrel source from {@link generateAdoptBarrel}
|
|
54
|
+
* @param flowsDir - Flows directory relative to root (default `"src/flows"`)
|
|
55
|
+
*/
|
|
56
|
+
export async function writeAdoptBarrel(
|
|
57
|
+
rootDir: string,
|
|
58
|
+
source: string,
|
|
59
|
+
flowsDir = "src/flows",
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
const dir = join(rootDir, flowsDir);
|
|
62
|
+
const target = join(dir, ADOPT_BARREL_FILE);
|
|
63
|
+
const tmp = `${target}.tmp`;
|
|
64
|
+
await mkdir(dir, { recursive: true });
|
|
65
|
+
await writeFile(tmp, source);
|
|
66
|
+
await rename(tmp, target);
|
|
67
|
+
}
|
|
68
|
+
|
|
43
69
|
/**
|
|
44
70
|
* Scan `<rootDir>/<flowsDir>/*` and emit a barrel re-exporting every unit
|
|
45
71
|
* folder's `index.ts` under its folder name — the same shape
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Driver-map defaults — per-key merge onto the real established defaults,
|
|
3
|
+
* never a whole-object replace and never leaking across driver maps.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from "bun:test";
|
|
7
|
+
import { mergeEnvDriverMap } from "./index.ts";
|
|
8
|
+
import {
|
|
9
|
+
CHANNEL_EMAIL_DEFAULTS,
|
|
10
|
+
CHANNEL_SMS_DEFAULTS,
|
|
11
|
+
CLOCK_DEFAULTS,
|
|
12
|
+
JOURNAL_DEFAULTS,
|
|
13
|
+
resolveEffectiveDrivers,
|
|
14
|
+
SIGNAL_DEFAULTS,
|
|
15
|
+
STORE_FILES_DEFAULTS,
|
|
16
|
+
STORE_KV_DEFAULTS,
|
|
17
|
+
STORE_SQL_DEFAULTS,
|
|
18
|
+
VAULT_DEFAULTS,
|
|
19
|
+
} from "./driver-defaults.ts";
|
|
20
|
+
|
|
21
|
+
describe("mergeEnvDriverMap", () => {
|
|
22
|
+
test("an unset key keeps the real default, not a sibling key's value", () => {
|
|
23
|
+
const merged = mergeEnvDriverMap({ local: "pglite" }, STORE_SQL_DEFAULTS);
|
|
24
|
+
expect(merged).toEqual({
|
|
25
|
+
local: "pglite",
|
|
26
|
+
docker: "postgres",
|
|
27
|
+
test: "memory",
|
|
28
|
+
prod: "postgres",
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("undefined override yields the untouched default map", () => {
|
|
33
|
+
expect(mergeEnvDriverMap(undefined, STORE_KV_DEFAULTS)).toEqual(STORE_KV_DEFAULTS);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("a fully-specified override wins on every key", () => {
|
|
37
|
+
const override = { local: "a", docker: "b", test: "c", prod: "d" };
|
|
38
|
+
expect(mergeEnvDriverMap(override, STORE_SQL_DEFAULTS)).toEqual(override);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("resolveEffectiveDrivers — the drivers.store.sql.local discussion scenario", () => {
|
|
43
|
+
test("`{ store: { sql: { local: 'pglite' } } }` resolves sql to the real map, pglite only on local", () => {
|
|
44
|
+
const effective = resolveEffectiveDrivers({
|
|
45
|
+
store: { sql: { local: "pglite" } },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(effective.store.sql).toEqual({
|
|
49
|
+
local: "pglite",
|
|
50
|
+
docker: "postgres",
|
|
51
|
+
test: "memory",
|
|
52
|
+
prod: "postgres",
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("every other driver stays at its full, real, untouched default map", () => {
|
|
57
|
+
const effective = resolveEffectiveDrivers({
|
|
58
|
+
store: { sql: { local: "pglite" } },
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(effective.store.kv).toEqual(STORE_KV_DEFAULTS);
|
|
62
|
+
expect(effective.store.files).toEqual(STORE_FILES_DEFAULTS);
|
|
63
|
+
expect(effective.signal).toEqual(SIGNAL_DEFAULTS);
|
|
64
|
+
expect(effective.clock).toEqual(CLOCK_DEFAULTS);
|
|
65
|
+
expect(effective.journal).toEqual(JOURNAL_DEFAULTS);
|
|
66
|
+
expect(effective.vault).toEqual(VAULT_DEFAULTS);
|
|
67
|
+
expect(effective.channel.email).toEqual(CHANNEL_EMAIL_DEFAULTS);
|
|
68
|
+
expect(effective.channel.sms).toEqual(CHANNEL_SMS_DEFAULTS);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("overriding one driver's map has zero effect on a sibling driver's defaults", () => {
|
|
72
|
+
const effective = resolveEffectiveDrivers({
|
|
73
|
+
store: { kv: { local: "redis" } },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// The overridden driver (kv) reflects the override on its one set key.
|
|
77
|
+
expect(effective.store.kv).toEqual({
|
|
78
|
+
local: "redis",
|
|
79
|
+
docker: "redis",
|
|
80
|
+
test: "memory",
|
|
81
|
+
prod: "redis",
|
|
82
|
+
});
|
|
83
|
+
// sql / files (same element, different facet) stay fully at real defaults.
|
|
84
|
+
expect(effective.store.sql).toEqual(STORE_SQL_DEFAULTS);
|
|
85
|
+
expect(effective.store.files).toEqual(STORE_FILES_DEFAULTS);
|
|
86
|
+
// Unrelated elements stay fully at real defaults too.
|
|
87
|
+
expect(effective.signal).toEqual(SIGNAL_DEFAULTS);
|
|
88
|
+
expect(effective.vault).toEqual(VAULT_DEFAULTS);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("overriding one env key leaves the other three at real defaults for that driver", () => {
|
|
92
|
+
const effective = resolveEffectiveDrivers({
|
|
93
|
+
vault: { local: "keychain" },
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(effective.vault).toEqual({
|
|
97
|
+
local: "keychain",
|
|
98
|
+
docker: "env",
|
|
99
|
+
test: "memory",
|
|
100
|
+
prod: "env",
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("no drivers config at all yields the real, untouched defaults for every map", () => {
|
|
105
|
+
const effective = resolveEffectiveDrivers(undefined);
|
|
106
|
+
expect(effective).toEqual({
|
|
107
|
+
store: {
|
|
108
|
+
sql: STORE_SQL_DEFAULTS,
|
|
109
|
+
kv: STORE_KV_DEFAULTS,
|
|
110
|
+
files: STORE_FILES_DEFAULTS,
|
|
111
|
+
},
|
|
112
|
+
signal: SIGNAL_DEFAULTS,
|
|
113
|
+
clock: CLOCK_DEFAULTS,
|
|
114
|
+
journal: JOURNAL_DEFAULTS,
|
|
115
|
+
vault: VAULT_DEFAULTS,
|
|
116
|
+
channel: {
|
|
117
|
+
email: CHANNEL_EMAIL_DEFAULTS,
|
|
118
|
+
sms: CHANNEL_SMS_DEFAULTS,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real, already-established default driver ids per {@link ConfigEnv} —
|
|
3
|
+
* one source of truth for every driver map that has a single default table.
|
|
4
|
+
*
|
|
5
|
+
* These mirror the literal fallbacks that used to live inline in each
|
|
6
|
+
* `src/kernel/boot-bind/*.ts` resolver (`docker ? "postgres" : "memory"`,
|
|
7
|
+
* `env === "test" ? "frozen" : "memory"`, …). Kept here so
|
|
8
|
+
* {@link mergeEnvDriverMap} has one real map to merge a developer's partial
|
|
9
|
+
* override onto, instead of every resolver re-deriving its own default.
|
|
10
|
+
*
|
|
11
|
+
* `store.index`, `channel.whatsapp` / `push`, and `ai` are intentionally
|
|
12
|
+
* absent — they don't have one established default across all four envs
|
|
13
|
+
* (index/whatsapp/push default to nothing until configured; ai has no prod
|
|
14
|
+
* default at all and must be declared) and keep using
|
|
15
|
+
* {@link resolveDriverId}'s legacy cascade.
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
mergeEnvDriverMap,
|
|
21
|
+
normalizeEnvDriverMap,
|
|
22
|
+
type DriversConfig,
|
|
23
|
+
type EnvDriverMap,
|
|
24
|
+
type RawEnvDriverMap,
|
|
25
|
+
} from "./index.ts";
|
|
26
|
+
|
|
27
|
+
/** `drivers.store.sql` — Postgres for docker/prod, in-memory otherwise. */
|
|
28
|
+
export const STORE_SQL_DEFAULTS: EnvDriverMap = {
|
|
29
|
+
local: "memory",
|
|
30
|
+
docker: "postgres",
|
|
31
|
+
test: "memory",
|
|
32
|
+
prod: "postgres",
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** `drivers.store.kv` — Redis for docker/prod, in-memory otherwise. */
|
|
36
|
+
export const STORE_KV_DEFAULTS: EnvDriverMap = {
|
|
37
|
+
local: "memory",
|
|
38
|
+
docker: "redis",
|
|
39
|
+
test: "memory",
|
|
40
|
+
prod: "redis",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** `drivers.store.files` — S3 for docker/prod, in-memory otherwise. */
|
|
44
|
+
export const STORE_FILES_DEFAULTS: EnvDriverMap = {
|
|
45
|
+
local: "memory",
|
|
46
|
+
docker: "s3",
|
|
47
|
+
test: "memory",
|
|
48
|
+
prod: "s3",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** `drivers.signal` — in-memory bus everywhere. */
|
|
52
|
+
export const SIGNAL_DEFAULTS: EnvDriverMap = {
|
|
53
|
+
local: "memory",
|
|
54
|
+
docker: "memory",
|
|
55
|
+
test: "memory",
|
|
56
|
+
prod: "memory",
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** `drivers.clock` — frozen test clock in `test`, in-memory cron elsewhere. */
|
|
60
|
+
export const CLOCK_DEFAULTS: EnvDriverMap = {
|
|
61
|
+
local: "memory",
|
|
62
|
+
docker: "memory",
|
|
63
|
+
test: "frozen",
|
|
64
|
+
prod: "memory",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** `drivers.journal` — in-memory durable-run store everywhere. */
|
|
68
|
+
export const JOURNAL_DEFAULTS: EnvDriverMap = {
|
|
69
|
+
local: "memory",
|
|
70
|
+
docker: "memory",
|
|
71
|
+
test: "memory",
|
|
72
|
+
prod: "memory",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** `drivers.vault` — OS env vars everywhere except `test` (in-memory). */
|
|
76
|
+
export const VAULT_DEFAULTS: EnvDriverMap = {
|
|
77
|
+
local: "env",
|
|
78
|
+
docker: "env",
|
|
79
|
+
test: "memory",
|
|
80
|
+
prod: "env",
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** `drivers.channel.email` — SMTP for docker/prod, console inbox otherwise. */
|
|
84
|
+
export const CHANNEL_EMAIL_DEFAULTS: EnvDriverMap = {
|
|
85
|
+
local: "console",
|
|
86
|
+
docker: "smtp",
|
|
87
|
+
test: "console",
|
|
88
|
+
prod: "smtp",
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* `drivers.channel.sms` — no default provider in any env; SMS is opt-in
|
|
93
|
+
* only (an unconfigured env simply gets no SMS channel).
|
|
94
|
+
*/
|
|
95
|
+
export const CHANNEL_SMS_DEFAULTS: EnvDriverMap = {};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Effective default map for a driver whose real default also depends on the
|
|
99
|
+
* boot-time `docker` flag (real docker-compose infra available), not just
|
|
100
|
+
* the active {@link ConfigEnv} key — `store.sql` / `store.kv` /
|
|
101
|
+
* `store.files` / `channel.email` / the Gate KV facet. Real usage always
|
|
102
|
+
* pairs `docker: true` with `env: "docker"` / `"prod"`, so the static table
|
|
103
|
+
* (`docker`/`prod` → real infra id) applies as-is; with the flag `false`
|
|
104
|
+
* (`oke dev -l`, `test`, or a boot that deliberately decouples `env` from
|
|
105
|
+
* infra availability) every key falls back to the same safe, no-external-
|
|
106
|
+
* deps id the real table already uses for `test`.
|
|
107
|
+
*
|
|
108
|
+
* @param defaults - Real per-env default table (its `docker`/`prod` slots
|
|
109
|
+
* assume real infra is actually up)
|
|
110
|
+
* @param docker - Boot-time docker/compose-infra flag
|
|
111
|
+
*/
|
|
112
|
+
export function dockerFlagDefaults(defaults: EnvDriverMap, docker: boolean): EnvDriverMap {
|
|
113
|
+
if (docker) return defaults;
|
|
114
|
+
const safe = defaults.test;
|
|
115
|
+
return { local: safe, docker: safe, test: safe, prod: safe };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Every driver map with one established default table, grouped by element. */
|
|
119
|
+
export const DRIVER_DEFAULTS = {
|
|
120
|
+
store: {
|
|
121
|
+
sql: STORE_SQL_DEFAULTS,
|
|
122
|
+
kv: STORE_KV_DEFAULTS,
|
|
123
|
+
files: STORE_FILES_DEFAULTS,
|
|
124
|
+
},
|
|
125
|
+
signal: SIGNAL_DEFAULTS,
|
|
126
|
+
clock: CLOCK_DEFAULTS,
|
|
127
|
+
journal: JOURNAL_DEFAULTS,
|
|
128
|
+
vault: VAULT_DEFAULTS,
|
|
129
|
+
channel: {
|
|
130
|
+
email: CHANNEL_EMAIL_DEFAULTS,
|
|
131
|
+
sms: CHANNEL_SMS_DEFAULTS,
|
|
132
|
+
},
|
|
133
|
+
} as const;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Every driver map from {@link DRIVER_DEFAULTS}, fully resolved — the
|
|
137
|
+
* developer's `oke.config.ts` override merged onto the real default,
|
|
138
|
+
* per-key, for all four {@link ConfigEnv} slots at once. This is the "full
|
|
139
|
+
* picture" `oke doctor` prints: what boot actually uses for every env, even
|
|
140
|
+
* when the config on disk only pins one key.
|
|
141
|
+
*/
|
|
142
|
+
export interface EffectiveDriversConfig {
|
|
143
|
+
readonly store: {
|
|
144
|
+
readonly sql: EnvDriverMap;
|
|
145
|
+
readonly kv: EnvDriverMap;
|
|
146
|
+
readonly files: EnvDriverMap;
|
|
147
|
+
};
|
|
148
|
+
readonly signal: EnvDriverMap;
|
|
149
|
+
readonly clock: EnvDriverMap;
|
|
150
|
+
readonly journal: EnvDriverMap;
|
|
151
|
+
readonly vault: EnvDriverMap;
|
|
152
|
+
readonly channel: {
|
|
153
|
+
readonly email: EnvDriverMap;
|
|
154
|
+
readonly sms: EnvDriverMap;
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Merge every driver map in {@link DRIVER_DEFAULTS} against the matching
|
|
160
|
+
* slot of a loaded `oke.config.ts` `drivers` block.
|
|
161
|
+
*
|
|
162
|
+
* @param drivers - `drivers` block from a loaded `oke.config.ts` (already
|
|
163
|
+
* normalized if it came through `defineConfig`; normalized again here so
|
|
164
|
+
* an un-normalized/raw block is still handled)
|
|
165
|
+
*/
|
|
166
|
+
export function resolveEffectiveDrivers(
|
|
167
|
+
drivers: DriversConfig | undefined,
|
|
168
|
+
): EffectiveDriversConfig {
|
|
169
|
+
const merge = (map: EnvDriverMap | undefined, defaults: EnvDriverMap) =>
|
|
170
|
+
mergeEnvDriverMap(normalizeEnvDriverMap(map as RawEnvDriverMap), defaults);
|
|
171
|
+
return {
|
|
172
|
+
store: {
|
|
173
|
+
sql: merge(drivers?.store?.sql, STORE_SQL_DEFAULTS),
|
|
174
|
+
kv: merge(drivers?.store?.kv, STORE_KV_DEFAULTS),
|
|
175
|
+
files: merge(drivers?.store?.files, STORE_FILES_DEFAULTS),
|
|
176
|
+
},
|
|
177
|
+
signal: merge(drivers?.signal, SIGNAL_DEFAULTS),
|
|
178
|
+
clock: merge(drivers?.clock, CLOCK_DEFAULTS),
|
|
179
|
+
journal: merge(drivers?.journal, JOURNAL_DEFAULTS),
|
|
180
|
+
vault: merge(drivers?.vault, VAULT_DEFAULTS),
|
|
181
|
+
channel: {
|
|
182
|
+
email: merge(drivers?.channel?.email, CHANNEL_EMAIL_DEFAULTS),
|
|
183
|
+
sms: merge(drivers?.channel?.sms, CHANNEL_SMS_DEFAULTS),
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
package/src/config/index.ts
CHANGED
|
@@ -81,8 +81,55 @@ export interface DriversConfig {
|
|
|
81
81
|
readonly runs?: EnvDriverMap;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
/**
|
|
85
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Container image pins, nested the same way as {@link DriversConfig}:
|
|
86
|
+
* `store.*` / `channel.*` facets nest under their owning element; roles with
|
|
87
|
+
* no driver counterpart (`vault` picks a driver but pins its own image;
|
|
88
|
+
* `ai`, `pgdog`, `proxy` have no config-level element at all) stay flat.
|
|
89
|
+
*
|
|
90
|
+
* Always fully explicit — `images` has no hidden default merge. Omitted keys
|
|
91
|
+
* mean "no container for that role," never "inherit a default image."
|
|
92
|
+
*/
|
|
93
|
+
export interface ImagesConfig {
|
|
94
|
+
readonly store?: {
|
|
95
|
+
readonly sql?: string;
|
|
96
|
+
readonly kv?: string;
|
|
97
|
+
readonly files?: string;
|
|
98
|
+
readonly index?: string;
|
|
99
|
+
};
|
|
100
|
+
readonly channel?: {
|
|
101
|
+
readonly email?: string;
|
|
102
|
+
};
|
|
103
|
+
readonly vault?: string;
|
|
104
|
+
readonly ai?: string;
|
|
105
|
+
readonly pgdog?: string;
|
|
106
|
+
readonly proxy?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Flatten {@link ImagesConfig} to the dotted-role `Record<string, string>`
|
|
111
|
+
* every internal consumer (compose role matching, credentials, env prefixes)
|
|
112
|
+
* has always kept working with — the nesting is a config-surface / type-safety
|
|
113
|
+
* concern only, not an internal representation change.
|
|
114
|
+
*
|
|
115
|
+
* @param images - Nested config-surface images (or `undefined`)
|
|
116
|
+
*/
|
|
117
|
+
export function flattenImagesConfig(
|
|
118
|
+
images: ImagesConfig | undefined,
|
|
119
|
+
): Readonly<Record<string, string>> {
|
|
120
|
+
if (!images) return {};
|
|
121
|
+
const out: Record<string, string> = {};
|
|
122
|
+
if (images.store?.sql) out["store.sql"] = images.store.sql;
|
|
123
|
+
if (images.store?.kv) out["store.kv"] = images.store.kv;
|
|
124
|
+
if (images.store?.files) out["store.files"] = images.store.files;
|
|
125
|
+
if (images.store?.index) out["store.index"] = images.store.index;
|
|
126
|
+
if (images.channel?.email) out["channel.email"] = images.channel.email;
|
|
127
|
+
if (images.vault) out.vault = images.vault;
|
|
128
|
+
if (images.ai) out.ai = images.ai;
|
|
129
|
+
if (images.pgdog) out.pgdog = images.pgdog;
|
|
130
|
+
if (images.proxy) out.proxy = images.proxy;
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
86
133
|
|
|
87
134
|
/** i18n config. */
|
|
88
135
|
export interface I18nConfig {
|
|
@@ -293,6 +340,32 @@ export function normalizeDriversConfig(
|
|
|
293
340
|
};
|
|
294
341
|
}
|
|
295
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Merge a developer's partial {@link EnvDriverMap} onto the real default map
|
|
345
|
+
* for one specific driver — per environment key, never a whole-object
|
|
346
|
+
* replace. An unset key keeps that driver's own real default; it never
|
|
347
|
+
* inherits a sibling key's value (the old `docker → prod → local → test`
|
|
348
|
+
* cascade in {@link resolveDriverId} did that, which is exactly the bug this
|
|
349
|
+
* fixes — `{ local: "pglite" }` no longer leaks `pglite` into `docker`).
|
|
350
|
+
*
|
|
351
|
+
* Scoped strictly to the `{ local?, docker?, test?, prod? }` shape — not a
|
|
352
|
+
* generic deep merge, and never applied to a whole config object.
|
|
353
|
+
*
|
|
354
|
+
* @param override - Developer-supplied partial map (may be `undefined`)
|
|
355
|
+
* @param defaults - Real, already-established default map for this driver
|
|
356
|
+
*/
|
|
357
|
+
export function mergeEnvDriverMap(
|
|
358
|
+
override: EnvDriverMap | undefined,
|
|
359
|
+
defaults: EnvDriverMap,
|
|
360
|
+
): EnvDriverMap {
|
|
361
|
+
return {
|
|
362
|
+
local: override?.local ?? defaults.local,
|
|
363
|
+
docker: override?.docker ?? defaults.docker,
|
|
364
|
+
test: override?.test ?? defaults.test,
|
|
365
|
+
prod: override?.prod ?? defaults.prod,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
296
369
|
/**
|
|
297
370
|
* Fill missing `docker` pins from `prod` (compose infra ≈ production protocols).
|
|
298
371
|
*
|
|
@@ -365,22 +438,39 @@ export function defineConfig(config: OkeConfig): OkeConfig {
|
|
|
365
438
|
/**
|
|
366
439
|
* Resolve a driver id for an env from an {@link EnvDriverMap}.
|
|
367
440
|
*
|
|
368
|
-
* `
|
|
369
|
-
*
|
|
441
|
+
* With `defaults` given, the developer's map is merged onto it per-key via
|
|
442
|
+
* {@link mergeEnvDriverMap} and the active env's slot is read directly — a
|
|
443
|
+
* key the developer never set keeps that driver's real default, full stop.
|
|
444
|
+
* This is the path every driver map with an established default should use.
|
|
445
|
+
*
|
|
446
|
+
* Without `defaults` (legacy path — `store.index`, `channel.whatsapp` /
|
|
447
|
+
* `push`, `ai`, which have no single established default table): `docker`
|
|
448
|
+
* falls back to `prod` then `local` then `test` so existing configs keep
|
|
449
|
+
* working until an explicit `docker:` pin is added. Legacy `dev`/`stack`
|
|
450
|
+
* keys are normalized first either way.
|
|
370
451
|
*
|
|
371
452
|
* @param map - Env → driver map
|
|
372
453
|
* @param env - Active environment
|
|
454
|
+
* @param defaults - Real default map for this specific driver (per-key merge)
|
|
373
455
|
*/
|
|
374
456
|
export function resolveDriverId(
|
|
375
457
|
map: EnvDriverMap | RawEnvDriverMap | undefined,
|
|
376
458
|
env: ConfigEnv,
|
|
459
|
+
defaults?: EnvDriverMap,
|
|
377
460
|
): string | undefined {
|
|
461
|
+
const normalized = normalizeEnvDriverMap(map as RawEnvDriverMap);
|
|
462
|
+
if (defaults) {
|
|
463
|
+
const merged = mergeEnvDriverMap(normalized, defaults);
|
|
464
|
+
const ref = merged[env];
|
|
465
|
+
if (ref === undefined) return undefined;
|
|
466
|
+
return typeof ref === "string" ? ref : ref.driver;
|
|
467
|
+
}
|
|
378
468
|
if (!map) return undefined;
|
|
379
|
-
const
|
|
469
|
+
const resolved = normalized ?? map;
|
|
380
470
|
const ref =
|
|
381
471
|
env === "docker"
|
|
382
|
-
? (
|
|
383
|
-
: (
|
|
472
|
+
? (resolved.docker ?? resolved.prod ?? resolved.local ?? resolved.test)
|
|
473
|
+
: (resolved[env] ?? resolved.local ?? resolved.test);
|
|
384
474
|
if (ref === undefined) return undefined;
|
|
385
475
|
return typeof ref === "string" ? ref : ref.driver;
|
|
386
476
|
}
|