okengine 0.2.3 → 0.2.4
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 +30 -4
- package/docs/spec/example.md +12 -3
- package/docs/spec/four-applications.md +10 -3
- package/package.json +9 -40
- package/src/cli/dev-app-runner.ts +45 -6
- package/src/cli/dev.ts +136 -17
- package/src/cli/docker-cli.test.ts +10 -8
- package/src/cli/docker.ts +18 -7
- package/src/cli/hero-meta.test.ts +85 -0
- package/src/cli/hero-meta.ts +252 -0
- package/src/cli/load-config.images.test.ts +51 -0
- package/src/cli/load-config.ts +84 -6
- package/src/config/define-config.test.ts +61 -0
- package/src/config/index.ts +89 -6
- package/src/config/resolve-driver.test.ts +30 -0
- package/src/console/server/flows.ts +3 -1
- package/src/console/server/operator-db.test.ts +140 -2
- package/src/console/server/operator-db.ts +147 -1
- package/src/console/server/plugin.ts +20 -0
- package/src/console/server/serve.ts +8 -1
- package/src/console/server/state.ts +12 -1
- package/src/console/ui/dist/assets/{index-Bnf_3Hei.js → index-Dy4jht9P.js} +1 -1
- package/src/console/ui/dist/index.html +1 -1
- package/src/console/ui/shell/App.tsx +10 -2
- package/src/docker/compose.ts +45 -14
- package/src/docker/derive.ts +29 -9
- package/src/docker/docker.test.ts +28 -4
- package/src/docker/dockerfile.integration.test.ts +2 -0
- package/src/docker/index.ts +10 -0
- package/src/docker/stack-id.test.ts +86 -0
- package/src/docker/stack-id.ts +108 -0
- package/src/docker/stack.integration.test.ts +16 -7
- package/src/docker/types.ts +20 -1
- package/src/kernel/app.ts +39 -8
- package/src/kernel/boot-bind/store.test.ts +60 -0
- package/src/kernel/boot-bind/store.ts +142 -12
- package/src/kernel/boot.ts +28 -4
- package/src/mcp/server.ts +99 -57
- package/src/runtime/dev-request-log.test.ts +33 -0
- package/src/runtime/dev-request-log.ts +130 -0
- package/src/term.test.ts +98 -5
- package/src/term.ts +287 -6
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hero snapshot — profile, elements, encode/decode.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import type { OkeConfig } from "../config/index.ts";
|
|
7
|
+
import {
|
|
8
|
+
buildDevHeroSnapshot,
|
|
9
|
+
decodeHeroSnapshot,
|
|
10
|
+
encodeHeroSnapshot,
|
|
11
|
+
resolveDevProfile,
|
|
12
|
+
resolveHeroElements,
|
|
13
|
+
} from "./hero-meta.ts";
|
|
14
|
+
|
|
15
|
+
const SAMPLE: OkeConfig = {
|
|
16
|
+
drivers: {
|
|
17
|
+
store: {
|
|
18
|
+
sql: { dev: "sqlite", stack: "postgres", prod: "postgres" },
|
|
19
|
+
kv: { dev: "memory", stack: "redis", prod: "redis" },
|
|
20
|
+
},
|
|
21
|
+
signal: { dev: "memory", stack: "postgres", prod: "postgres" },
|
|
22
|
+
clock: { dev: "memory", stack: "postgres", prod: "postgres" },
|
|
23
|
+
vault: { dev: "dotenv", stack: "dotenv", prod: "sops" },
|
|
24
|
+
channel: {
|
|
25
|
+
email: { dev: "console", stack: "smtp", prod: "smtp" },
|
|
26
|
+
},
|
|
27
|
+
ai: { dev: "mock", stack: "mock" },
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
describe("hero-meta", () => {
|
|
32
|
+
test("resolveDevProfile maps stack and NODE_ENV", () => {
|
|
33
|
+
expect(resolveDevProfile({ stack: false })).toBe("local");
|
|
34
|
+
expect(resolveDevProfile({ stack: true })).toBe("local-server");
|
|
35
|
+
expect(resolveDevProfile({ stack: false, nodeEnv: "test" })).toBe("test");
|
|
36
|
+
expect(resolveDevProfile({ stack: false, nodeEnv: "production" })).toBe(
|
|
37
|
+
"production",
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("resolveHeroElements lists eight elements with drivers", () => {
|
|
42
|
+
const rows = resolveHeroElements(SAMPLE, { stack: false });
|
|
43
|
+
expect(rows.map((r) => r.element)).toEqual([
|
|
44
|
+
"flow",
|
|
45
|
+
"signal",
|
|
46
|
+
"store",
|
|
47
|
+
"clock",
|
|
48
|
+
"gate",
|
|
49
|
+
"vault",
|
|
50
|
+
"channel",
|
|
51
|
+
"ai",
|
|
52
|
+
]);
|
|
53
|
+
expect(rows.find((r) => r.element === "store")?.detail).toContain("sqlite");
|
|
54
|
+
expect(rows.find((r) => r.element === "flow")?.detail).toBe("●");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("stack profile applies to every element", () => {
|
|
58
|
+
const rows = resolveHeroElements(SAMPLE, {
|
|
59
|
+
stack: true,
|
|
60
|
+
sqlDriver: "postgres",
|
|
61
|
+
kvDriver: "redis",
|
|
62
|
+
});
|
|
63
|
+
const store = rows.find((r) => r.element === "store")?.detail ?? "";
|
|
64
|
+
expect(store).toContain("postgres");
|
|
65
|
+
expect(store).toContain("redis");
|
|
66
|
+
expect(store).not.toContain("sqlite");
|
|
67
|
+
expect(rows.find((r) => r.element === "signal")?.detail).toBe("postgres");
|
|
68
|
+
expect(rows.find((r) => r.element === "clock")?.detail).toBe("postgres");
|
|
69
|
+
expect(rows.find((r) => r.element === "channel")?.detail).toContain("smtp");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("encode/decode round-trips", () => {
|
|
73
|
+
const snap = buildDevHeroSnapshot({
|
|
74
|
+
config: SAMPLE,
|
|
75
|
+
stack: true,
|
|
76
|
+
sqlDriver: "postgres",
|
|
77
|
+
kvDriver: "redis",
|
|
78
|
+
version: "0.2.4",
|
|
79
|
+
nodeEnv: "development",
|
|
80
|
+
});
|
|
81
|
+
expect(snap.profile).toBe("local-server");
|
|
82
|
+
expect(snap.runtimeEnv).toBe("local");
|
|
83
|
+
expect(decodeHeroSnapshot(encodeHeroSnapshot(snap))).toEqual(snap);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve `oke dev` hero facts — eight elements, profile, env, system.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { release } from "node:os";
|
|
6
|
+
import {
|
|
7
|
+
resolveDriverId,
|
|
8
|
+
type ConfigEnv,
|
|
9
|
+
type DriversConfig,
|
|
10
|
+
type OkeConfig,
|
|
11
|
+
} from "../config/index.ts";
|
|
12
|
+
|
|
13
|
+
/** How the process is running (local laptop → production). */
|
|
14
|
+
export type DevRuntimeProfile =
|
|
15
|
+
| "local"
|
|
16
|
+
| "local-server"
|
|
17
|
+
| "test"
|
|
18
|
+
| "production";
|
|
19
|
+
|
|
20
|
+
/** Data plane: still on a laptop vs real production. */
|
|
21
|
+
export type DevRuntimeEnv = "local" | "production";
|
|
22
|
+
|
|
23
|
+
/** One of the eight elements with its active driver summary. */
|
|
24
|
+
export type HeroElementRow = {
|
|
25
|
+
readonly element: string;
|
|
26
|
+
/** Driver summary, or `—` when unbound. */
|
|
27
|
+
readonly detail: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Serializable hero payload (TTY + soft-reload env). */
|
|
31
|
+
export type DevHeroSnapshot = {
|
|
32
|
+
readonly profile: DevRuntimeProfile;
|
|
33
|
+
readonly runtimeEnv: DevRuntimeEnv;
|
|
34
|
+
readonly system: string;
|
|
35
|
+
readonly elements: readonly HeroElementRow[];
|
|
36
|
+
readonly version?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const EIGHT_ELEMENTS = [
|
|
40
|
+
"flow",
|
|
41
|
+
"signal",
|
|
42
|
+
"store",
|
|
43
|
+
"clock",
|
|
44
|
+
"gate",
|
|
45
|
+
"vault",
|
|
46
|
+
"channel",
|
|
47
|
+
"ai",
|
|
48
|
+
] as const;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Host / runtime line for the hero (`darwin 25.4.0 · bun 1.3.14`).
|
|
52
|
+
*/
|
|
53
|
+
export function formatHeroSystemLine(
|
|
54
|
+
bunVersion: string = Bun.version,
|
|
55
|
+
platform: string = process.platform,
|
|
56
|
+
osRelease: string = release(),
|
|
57
|
+
): string {
|
|
58
|
+
return `${platform} ${osRelease} · bun ${bunVersion}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Profile for an `oke dev` session.
|
|
63
|
+
*
|
|
64
|
+
* @param options - Stack / NODE_ENV hints
|
|
65
|
+
*/
|
|
66
|
+
export function resolveDevProfile(options: {
|
|
67
|
+
readonly stack: boolean;
|
|
68
|
+
/** Explicit role — do not infer from ambient `NODE_ENV` (tests set `test`). */
|
|
69
|
+
readonly nodeEnv?: string;
|
|
70
|
+
}): DevRuntimeProfile {
|
|
71
|
+
if (options.nodeEnv === "test") return "test";
|
|
72
|
+
if (options.nodeEnv === "production") return "production";
|
|
73
|
+
return options.stack ? "local-server" : "local";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Data-plane environment (local machine vs production deploy).
|
|
78
|
+
*
|
|
79
|
+
* @param profile - Runtime profile
|
|
80
|
+
*/
|
|
81
|
+
export function resolveDevRuntimeEnv(
|
|
82
|
+
profile: DevRuntimeProfile,
|
|
83
|
+
): DevRuntimeEnv {
|
|
84
|
+
return profile === "production" ? "production" : "local";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Active driver id for an env map, preferring {@link prefer} when set.
|
|
89
|
+
*
|
|
90
|
+
* @param map - Env driver map
|
|
91
|
+
* @param env - Config env key
|
|
92
|
+
* @param prefer - Override (stack sql/kv)
|
|
93
|
+
*/
|
|
94
|
+
function driverOr(
|
|
95
|
+
map: Parameters<typeof resolveDriverId>[0],
|
|
96
|
+
env: ConfigEnv,
|
|
97
|
+
prefer?: string,
|
|
98
|
+
): string | undefined {
|
|
99
|
+
if (prefer !== undefined && prefer.length > 0) return prefer;
|
|
100
|
+
return resolveDriverId(map, env);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Summarize channel mediums for the active config env.
|
|
105
|
+
*
|
|
106
|
+
* @param channel - Channel driver maps
|
|
107
|
+
* @param env - Config env
|
|
108
|
+
*/
|
|
109
|
+
function channelDetail(
|
|
110
|
+
channel: DriversConfig["channel"],
|
|
111
|
+
env: ConfigEnv,
|
|
112
|
+
): string {
|
|
113
|
+
if (!channel) return "—";
|
|
114
|
+
const parts: string[] = [];
|
|
115
|
+
for (const medium of ["email", "sms", "whatsapp", "push"] as const) {
|
|
116
|
+
const id = resolveDriverId(channel[medium], env);
|
|
117
|
+
if (id) parts.push(`${medium} ${id}`);
|
|
118
|
+
}
|
|
119
|
+
return parts.length > 0 ? parts.join(" · ") : "—";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Summarize store facets for the active (or stack-overridden) drivers.
|
|
124
|
+
*
|
|
125
|
+
* @param store - Store driver maps
|
|
126
|
+
* @param env - Config env for non-overridden facets
|
|
127
|
+
* @param overrides - Stack sql/kv ids
|
|
128
|
+
*/
|
|
129
|
+
function storeDetail(
|
|
130
|
+
store: DriversConfig["store"],
|
|
131
|
+
env: ConfigEnv,
|
|
132
|
+
overrides: { readonly sql?: string; readonly kv?: string },
|
|
133
|
+
): string {
|
|
134
|
+
if (!store && !overrides.sql && !overrides.kv) return "—";
|
|
135
|
+
const parts: string[] = [];
|
|
136
|
+
const sql = driverOr(store?.sql, env, overrides.sql);
|
|
137
|
+
const kv = driverOr(store?.kv, env, overrides.kv);
|
|
138
|
+
const files = resolveDriverId(store?.files, env);
|
|
139
|
+
const index = resolveDriverId(store?.index, env);
|
|
140
|
+
if (sql) parts.push(`sql ${sql}`);
|
|
141
|
+
if (kv) parts.push(`kv ${kv}`);
|
|
142
|
+
if (files) parts.push(`files ${files}`);
|
|
143
|
+
if (index) parts.push(`index ${index}`);
|
|
144
|
+
return parts.length > 0 ? parts.join(" · ") : "—";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Resolve eight-element rows from `oke.config` + stack overrides.
|
|
149
|
+
*
|
|
150
|
+
* @param config - Loaded config (or null)
|
|
151
|
+
* @param options - Stack driver overrides; config env for maps
|
|
152
|
+
*/
|
|
153
|
+
export function resolveHeroElements(
|
|
154
|
+
config: OkeConfig | null | undefined,
|
|
155
|
+
options: {
|
|
156
|
+
/** Use prod store ids from stack (`oke dev -s`). */
|
|
157
|
+
readonly stack: boolean;
|
|
158
|
+
readonly sqlDriver?: string;
|
|
159
|
+
readonly kvDriver?: string;
|
|
160
|
+
/** Which driver map column to read (default `dev`). */
|
|
161
|
+
readonly configEnv?: ConfigEnv;
|
|
162
|
+
},
|
|
163
|
+
): readonly HeroElementRow[] {
|
|
164
|
+
const drivers = config?.drivers;
|
|
165
|
+
// `-s` reads the `stack` profile for every element (not a store-only override).
|
|
166
|
+
const configEnv: ConfigEnv =
|
|
167
|
+
options.configEnv ?? (options.stack ? "stack" : "dev");
|
|
168
|
+
|
|
169
|
+
const byName: Record<string, string> = {
|
|
170
|
+
flow: "●",
|
|
171
|
+
signal: resolveDriverId(drivers?.signal, configEnv) ?? "—",
|
|
172
|
+
store: storeDetail(drivers?.store, configEnv, {
|
|
173
|
+
sql: options.sqlDriver,
|
|
174
|
+
kv: options.kvDriver,
|
|
175
|
+
}),
|
|
176
|
+
clock: resolveDriverId(drivers?.clock, configEnv) ?? "—",
|
|
177
|
+
gate: "—",
|
|
178
|
+
vault: resolveDriverId(drivers?.vault, configEnv) ?? "—",
|
|
179
|
+
channel: channelDetail(drivers?.channel, configEnv),
|
|
180
|
+
ai: resolveDriverId(drivers?.ai, configEnv) ?? "—",
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
return EIGHT_ELEMENTS.map((element) => ({
|
|
184
|
+
element,
|
|
185
|
+
detail: byName[element] ?? "—",
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Build the full hero snapshot for banner / soft reload.
|
|
191
|
+
*
|
|
192
|
+
* @param options - Config, stack, versions
|
|
193
|
+
*/
|
|
194
|
+
export function buildDevHeroSnapshot(options: {
|
|
195
|
+
readonly config?: OkeConfig | null;
|
|
196
|
+
readonly stack: boolean;
|
|
197
|
+
readonly sqlDriver?: string;
|
|
198
|
+
readonly kvDriver?: string;
|
|
199
|
+
readonly version?: string;
|
|
200
|
+
readonly nodeEnv?: string;
|
|
201
|
+
}): DevHeroSnapshot {
|
|
202
|
+
const profile = resolveDevProfile({
|
|
203
|
+
stack: options.stack,
|
|
204
|
+
nodeEnv: options.nodeEnv,
|
|
205
|
+
});
|
|
206
|
+
return {
|
|
207
|
+
profile,
|
|
208
|
+
runtimeEnv: resolveDevRuntimeEnv(profile),
|
|
209
|
+
system: formatHeroSystemLine(),
|
|
210
|
+
elements: resolveHeroElements(options.config, {
|
|
211
|
+
stack: options.stack,
|
|
212
|
+
sqlDriver: options.sqlDriver,
|
|
213
|
+
kvDriver: options.kvDriver,
|
|
214
|
+
}),
|
|
215
|
+
...(options.version !== undefined ? { version: options.version } : {}),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Encode snapshot for the app child soft-reload env.
|
|
221
|
+
*
|
|
222
|
+
* @param snapshot - Hero facts
|
|
223
|
+
*/
|
|
224
|
+
export function encodeHeroSnapshot(snapshot: DevHeroSnapshot): string {
|
|
225
|
+
return JSON.stringify(snapshot);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Decode soft-reload hero env (invalid → null).
|
|
230
|
+
*
|
|
231
|
+
* @param raw - JSON from `OKE_DEV_HERO_META`
|
|
232
|
+
*/
|
|
233
|
+
export function decodeHeroSnapshot(raw: string | undefined): DevHeroSnapshot | null {
|
|
234
|
+
if (raw === undefined || raw.length === 0) return null;
|
|
235
|
+
try {
|
|
236
|
+
const parsed: unknown = JSON.parse(raw);
|
|
237
|
+
if (
|
|
238
|
+
typeof parsed !== "object" ||
|
|
239
|
+
parsed === null ||
|
|
240
|
+
!("profile" in parsed) ||
|
|
241
|
+
!("runtimeEnv" in parsed) ||
|
|
242
|
+
!("system" in parsed) ||
|
|
243
|
+
!("elements" in parsed) ||
|
|
244
|
+
!Array.isArray((parsed as DevHeroSnapshot).elements)
|
|
245
|
+
) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
return parsed as DevHeroSnapshot;
|
|
249
|
+
} catch {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -6,6 +6,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
6
6
|
import {
|
|
7
7
|
defaultImagesFromConfig,
|
|
8
8
|
resolveImages,
|
|
9
|
+
stackDevDriverMismatches,
|
|
9
10
|
} from "./load-config.ts";
|
|
10
11
|
|
|
11
12
|
describe("defaultImagesFromConfig", () => {
|
|
@@ -73,3 +74,53 @@ describe("resolveImages", () => {
|
|
|
73
74
|
expect(images["store.kv"]).toBe("redis:8-alpine");
|
|
74
75
|
});
|
|
75
76
|
});
|
|
77
|
+
|
|
78
|
+
describe("stackDevDriverMismatches", () => {
|
|
79
|
+
test("flags stack/prod sqlite/memory when containers are up", () => {
|
|
80
|
+
const mismatches = stackDevDriverMismatches(
|
|
81
|
+
{
|
|
82
|
+
drivers: {
|
|
83
|
+
store: {
|
|
84
|
+
sql: { dev: "sqlite", stack: "sqlite", prod: "sqlite" },
|
|
85
|
+
kv: { dev: "memory", stack: "memory", prod: "memory" },
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
["store.sql", "store.kv"],
|
|
90
|
+
);
|
|
91
|
+
expect(mismatches).toEqual([
|
|
92
|
+
{ label: "sql", using: "sqlite", container: "postgres" },
|
|
93
|
+
{ label: "kv", using: "memory", container: "redis" },
|
|
94
|
+
]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("silent when stack profile matches containers", () => {
|
|
98
|
+
const mismatches = stackDevDriverMismatches(
|
|
99
|
+
{
|
|
100
|
+
drivers: {
|
|
101
|
+
store: {
|
|
102
|
+
sql: { dev: "sqlite", stack: "postgres", prod: "postgres" },
|
|
103
|
+
kv: { dev: "memory", stack: "redis", prod: "redis" },
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
["store.sql", "store.kv"],
|
|
108
|
+
);
|
|
109
|
+
expect(mismatches).toEqual([]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("silent when only prod is set (stack falls back to prod)", () => {
|
|
113
|
+
const mismatches = stackDevDriverMismatches(
|
|
114
|
+
{
|
|
115
|
+
drivers: {
|
|
116
|
+
store: {
|
|
117
|
+
sql: { dev: "sqlite", prod: "postgres" },
|
|
118
|
+
kv: { dev: "memory", prod: "redis" },
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
["store.sql", "store.kv"],
|
|
123
|
+
);
|
|
124
|
+
expect(mismatches).toEqual([]);
|
|
125
|
+
});
|
|
126
|
+
});
|
package/src/cli/load-config.ts
CHANGED
|
@@ -22,7 +22,18 @@ function driverId(ref: DriverRef | undefined): string | undefined {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* Prefer `stack` then `prod` for a driver map (local server ≈ production).
|
|
26
|
+
*
|
|
27
|
+
* @param map - Env driver map
|
|
28
|
+
*/
|
|
29
|
+
function stackOrProdId(
|
|
30
|
+
map: { readonly stack?: DriverRef; readonly prod?: DriverRef } | undefined,
|
|
31
|
+
): string | undefined {
|
|
32
|
+
return driverId(map?.stack) ?? driverId(map?.prod);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Derive default image pins from stack/prod driver protocols.
|
|
26
37
|
*
|
|
27
38
|
* Used when `oke.config.ts` omits `images` but declares postgres/redis (etc.)
|
|
28
39
|
* so `oke dev -s` / `oke stack` / `oke docker` have something to run.
|
|
@@ -33,11 +44,11 @@ export function defaultImagesFromConfig(
|
|
|
33
44
|
config: OkeConfig,
|
|
34
45
|
): Readonly<Record<string, string>> {
|
|
35
46
|
const out: Record<string, string> = {};
|
|
36
|
-
const sql =
|
|
37
|
-
const index =
|
|
38
|
-
const kv =
|
|
39
|
-
const signal =
|
|
40
|
-
const clock =
|
|
47
|
+
const sql = stackOrProdId(config.drivers?.store?.sql);
|
|
48
|
+
const index = stackOrProdId(config.drivers?.store?.index);
|
|
49
|
+
const kv = stackOrProdId(config.drivers?.store?.kv);
|
|
50
|
+
const signal = stackOrProdId(config.drivers?.signal);
|
|
51
|
+
const clock = stackOrProdId(config.drivers?.clock);
|
|
41
52
|
|
|
42
53
|
const needsSql =
|
|
43
54
|
sql === "postgres" ||
|
|
@@ -134,3 +145,70 @@ export function resolveImages(
|
|
|
134
145
|
if (config) return defaultImagesFromConfig(config);
|
|
135
146
|
return {};
|
|
136
147
|
}
|
|
148
|
+
|
|
149
|
+
/** Compact driver mismatch for {@link formatStackSummary}. */
|
|
150
|
+
export type StackDriverMismatch = {
|
|
151
|
+
/** Short label (`sql`, `kv`). */
|
|
152
|
+
readonly label: string;
|
|
153
|
+
/** Active `drivers.*.stack` (→ prod) id. */
|
|
154
|
+
readonly using: string;
|
|
155
|
+
/** Driver that would use the container. */
|
|
156
|
+
readonly container: string;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Detect when `oke dev -s` containers will not back the app because the
|
|
161
|
+
* `stack` driver profile still points at local/memory backends.
|
|
162
|
+
*
|
|
163
|
+
* @param config - Loaded config
|
|
164
|
+
* @param stackRoles - Roles being composed
|
|
165
|
+
*/
|
|
166
|
+
export function stackDevDriverMismatches(
|
|
167
|
+
config: OkeConfig,
|
|
168
|
+
stackRoles: readonly string[],
|
|
169
|
+
): readonly StackDriverMismatch[] {
|
|
170
|
+
const roles = new Set(stackRoles);
|
|
171
|
+
const out: StackDriverMismatch[] = [];
|
|
172
|
+
const sqlStack =
|
|
173
|
+
driverId(config.drivers?.store?.sql?.stack) ??
|
|
174
|
+
driverId(config.drivers?.store?.sql?.prod);
|
|
175
|
+
const kvStack =
|
|
176
|
+
driverId(config.drivers?.store?.kv?.stack) ??
|
|
177
|
+
driverId(config.drivers?.store?.kv?.prod);
|
|
178
|
+
|
|
179
|
+
if (
|
|
180
|
+
roles.has("store.sql") &&
|
|
181
|
+
(sqlStack === "sqlite" || sqlStack === "memory" || sqlStack === undefined)
|
|
182
|
+
) {
|
|
183
|
+
out.push({
|
|
184
|
+
label: "sql",
|
|
185
|
+
using: sqlStack ?? "unset",
|
|
186
|
+
container: "postgres",
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (
|
|
190
|
+
roles.has("store.kv") &&
|
|
191
|
+
(kvStack === "memory" || kvStack === undefined)
|
|
192
|
+
) {
|
|
193
|
+
out.push({
|
|
194
|
+
label: "kv",
|
|
195
|
+
using: kvStack ?? "memory",
|
|
196
|
+
container: "redis",
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @deprecated Use {@link stackDevDriverMismatches} — kept for older call sites.
|
|
204
|
+
* @param config - Loaded config
|
|
205
|
+
* @param stackRoles - Roles being composed
|
|
206
|
+
*/
|
|
207
|
+
export function stackDevDriverWarnings(
|
|
208
|
+
config: OkeConfig,
|
|
209
|
+
stackRoles: readonly string[],
|
|
210
|
+
): readonly string[] {
|
|
211
|
+
return stackDevDriverMismatches(config, stackRoles).map(
|
|
212
|
+
(m) => `${m.label}: ${m.using}`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `defineConfig` fills missing `stack` pins from `prod`.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { defineConfig, fillStackFromProd } from "./index.ts";
|
|
7
|
+
|
|
8
|
+
describe("fillStackFromProd / defineConfig", () => {
|
|
9
|
+
test("copies prod onto stack when stack is omitted", () => {
|
|
10
|
+
const filled = fillStackFromProd({
|
|
11
|
+
dev: "sqlite",
|
|
12
|
+
prod: "postgres",
|
|
13
|
+
});
|
|
14
|
+
expect(filled?.stack).toBe("postgres");
|
|
15
|
+
expect(filled?.dev).toBe("sqlite");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("does not overwrite an explicit stack pin", () => {
|
|
19
|
+
expect(
|
|
20
|
+
fillStackFromProd({
|
|
21
|
+
dev: "sqlite",
|
|
22
|
+
stack: "memory",
|
|
23
|
+
prod: "postgres",
|
|
24
|
+
})?.stack,
|
|
25
|
+
).toBe("memory");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("vault sops → stack dotenv", () => {
|
|
29
|
+
expect(
|
|
30
|
+
fillStackFromProd({ dev: "dotenv", prod: "sops" }, { vault: true })
|
|
31
|
+
?.stack,
|
|
32
|
+
).toBe("dotenv");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("defineConfig fills every element from prod", () => {
|
|
36
|
+
const cfg = defineConfig({
|
|
37
|
+
drivers: {
|
|
38
|
+
store: {
|
|
39
|
+
sql: { dev: "sqlite", prod: "postgres" },
|
|
40
|
+
kv: { dev: "memory", prod: "redis" },
|
|
41
|
+
files: { dev: "fs", prod: "s3" },
|
|
42
|
+
},
|
|
43
|
+
signal: { dev: "memory", prod: "postgres" },
|
|
44
|
+
clock: { dev: "memory", prod: "postgres" },
|
|
45
|
+
vault: { dev: "dotenv", prod: "sops" },
|
|
46
|
+
channel: {
|
|
47
|
+
email: { dev: "console", prod: "smtp" },
|
|
48
|
+
},
|
|
49
|
+
ai: { dev: "mock", prod: "anthropic" },
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
expect(cfg.drivers?.store?.sql?.stack).toBe("postgres");
|
|
53
|
+
expect(cfg.drivers?.store?.kv?.stack).toBe("redis");
|
|
54
|
+
expect(cfg.drivers?.store?.files?.stack).toBe("s3");
|
|
55
|
+
expect(cfg.drivers?.signal?.stack).toBe("postgres");
|
|
56
|
+
expect(cfg.drivers?.clock?.stack).toBe("postgres");
|
|
57
|
+
expect(cfg.drivers?.vault?.stack).toBe("dotenv");
|
|
58
|
+
expect(cfg.drivers?.channel?.email?.stack).toBe("smtp");
|
|
59
|
+
expect(cfg.drivers?.ai?.stack).toBe("anthropic");
|
|
60
|
+
});
|
|
61
|
+
});
|
package/src/config/index.ts
CHANGED
|
@@ -5,8 +5,15 @@
|
|
|
5
5
|
* @module
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
/**
|
|
9
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Environment role keys used in driver maps.
|
|
10
|
+
*
|
|
11
|
+
* - `dev` — local laptop (`oke dev`)
|
|
12
|
+
* - `stack` — local server (`oke dev -s` / compose infra + host Bun)
|
|
13
|
+
* - `test` — automated tests
|
|
14
|
+
* - `prod` — production deploy
|
|
15
|
+
*/
|
|
16
|
+
export type ConfigEnv = "dev" | "stack" | "test" | "prod";
|
|
10
17
|
|
|
11
18
|
/** Pool options for SQL drivers. */
|
|
12
19
|
export interface DriverPoolOptions {
|
|
@@ -143,17 +150,90 @@ export interface OkeConfig {
|
|
|
143
150
|
}
|
|
144
151
|
|
|
145
152
|
/**
|
|
146
|
-
*
|
|
153
|
+
* Fill missing `stack` pins from `prod` (local server ≈ production protocols).
|
|
154
|
+
*
|
|
155
|
+
* Vault is the exception: `prod: "sops"` becomes `stack: "dotenv"` so
|
|
156
|
+
* `oke dev -s` can read `.env.stack` without age keys.
|
|
157
|
+
*
|
|
158
|
+
* @param map - Env → driver map
|
|
159
|
+
* @param options - Element-specific defaults
|
|
160
|
+
*/
|
|
161
|
+
export function fillStackFromProd(
|
|
162
|
+
map: EnvDriverMap | undefined,
|
|
163
|
+
options: { readonly vault?: boolean } = {},
|
|
164
|
+
): EnvDriverMap | undefined {
|
|
165
|
+
if (!map) return undefined;
|
|
166
|
+
if (map.stack !== undefined || map.prod === undefined) return map;
|
|
167
|
+
if (options.vault) {
|
|
168
|
+
const prodId = typeof map.prod === "string" ? map.prod : map.prod.driver;
|
|
169
|
+
if (prodId === "sops") {
|
|
170
|
+
return { ...map, stack: "dotenv" };
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return { ...map, stack: map.prod };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Copy production driver pins onto `stack` wherever `stack` is omitted.
|
|
178
|
+
*
|
|
179
|
+
* @param drivers - Drivers block
|
|
180
|
+
*/
|
|
181
|
+
export function fillDriversStackFromProd(
|
|
182
|
+
drivers: DriversConfig | undefined,
|
|
183
|
+
): DriversConfig | undefined {
|
|
184
|
+
if (!drivers) return undefined;
|
|
185
|
+
const store = drivers.store
|
|
186
|
+
? {
|
|
187
|
+
...drivers.store,
|
|
188
|
+
sql: fillStackFromProd(drivers.store.sql),
|
|
189
|
+
kv: fillStackFromProd(drivers.store.kv),
|
|
190
|
+
files: fillStackFromProd(drivers.store.files),
|
|
191
|
+
index: fillStackFromProd(drivers.store.index),
|
|
192
|
+
}
|
|
193
|
+
: undefined;
|
|
194
|
+
const channel = drivers.channel
|
|
195
|
+
? {
|
|
196
|
+
...drivers.channel,
|
|
197
|
+
email: fillStackFromProd(drivers.channel.email),
|
|
198
|
+
sms: fillStackFromProd(drivers.channel.sms),
|
|
199
|
+
whatsapp: fillStackFromProd(drivers.channel.whatsapp),
|
|
200
|
+
push: fillStackFromProd(drivers.channel.push),
|
|
201
|
+
}
|
|
202
|
+
: undefined;
|
|
203
|
+
return {
|
|
204
|
+
...drivers,
|
|
205
|
+
...(store !== undefined ? { store } : {}),
|
|
206
|
+
signal: fillStackFromProd(drivers.signal),
|
|
207
|
+
clock: fillStackFromProd(drivers.clock),
|
|
208
|
+
vault: fillStackFromProd(drivers.vault, { vault: true }),
|
|
209
|
+
...(channel !== undefined ? { channel } : {}),
|
|
210
|
+
ai: fillStackFromProd(drivers.ai),
|
|
211
|
+
runs: fillStackFromProd(drivers.runs),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Define an `oke.config.ts` document.
|
|
217
|
+
*
|
|
218
|
+
* Missing `stack` driver pins are filled from `prod` so `oke dev -s` uses the
|
|
219
|
+
* same server protocols as production (vault → dotenv for `.env.stack`).
|
|
147
220
|
*
|
|
148
221
|
* @param config - Driver / tenancy / i18n / topology
|
|
149
222
|
*/
|
|
150
|
-
export function defineConfig
|
|
151
|
-
return config;
|
|
223
|
+
export function defineConfig(config: OkeConfig): OkeConfig {
|
|
224
|
+
if (!config.drivers) return config;
|
|
225
|
+
return {
|
|
226
|
+
...config,
|
|
227
|
+
drivers: fillDriversStackFromProd(config.drivers),
|
|
228
|
+
};
|
|
152
229
|
}
|
|
153
230
|
|
|
154
231
|
/**
|
|
155
232
|
* Resolve a driver id for an env from an {@link EnvDriverMap}.
|
|
156
233
|
*
|
|
234
|
+
* `stack` falls back to `prod` then `dev` so existing configs keep working
|
|
235
|
+
* until an explicit `stack:` pin is added.
|
|
236
|
+
*
|
|
157
237
|
* @param map - Env → driver map
|
|
158
238
|
* @param env - Active environment
|
|
159
239
|
*/
|
|
@@ -162,7 +242,10 @@ export function resolveDriverId(
|
|
|
162
242
|
env: ConfigEnv,
|
|
163
243
|
): string | undefined {
|
|
164
244
|
if (!map) return undefined;
|
|
165
|
-
const ref =
|
|
245
|
+
const ref =
|
|
246
|
+
env === "stack"
|
|
247
|
+
? (map.stack ?? map.prod ?? map.dev ?? map.test)
|
|
248
|
+
: (map[env] ?? map.dev ?? map.test);
|
|
166
249
|
if (ref === undefined) return undefined;
|
|
167
250
|
return typeof ref === "string" ? ref : ref.driver;
|
|
168
251
|
}
|