okengine 0.2.2 → 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 +204 -23
- package/src/cli/docker-cli.test.ts +10 -8
- package/src/cli/docker.ts +18 -7
- package/src/cli/doctor.ts +3 -18
- package/src/cli/hero-meta.test.ts +85 -0
- package/src/cli/hero-meta.ts +252 -0
- package/src/cli/index.ts +4 -1
- package/src/cli/json-out.test.ts +1 -1
- package/src/cli/load-config.images.test.ts +126 -0
- package/src/cli/load-config.ts +145 -2
- package/src/cli/ports.test.ts +51 -0
- package/src/cli/ports.ts +81 -0
- package/src/cli/registry.help.test.ts +18 -0
- package/src/cli/registry.ts +0 -4
- package/src/cli/safe-defaults.test.ts +3 -3
- 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/app.ts +2 -1
- package/src/console/server/flows.ts +4 -1
- package/src/console/server/index.ts +5 -0
- package/src/console/server/operator-db.test.ts +213 -0
- package/src/console/server/operator-db.ts +470 -0
- package/src/console/server/plugin.ts +20 -0
- package/src/console/server/serve.ts +47 -1
- package/src/console/server/state.ts +25 -2
- package/src/console/ui/dist/assets/{index-B71Yl_SS.js → index-Dy4jht9P.js} +3 -3
- package/src/console/ui/dist/assets/{panel-overview-Bd48d9km.js → panel-overview-Dt_AeXgd.js} +1 -1
- package/src/console/ui/dist/assets/{panel-runs-BwsWqKeB.js → panel-runs-DGstFHeq.js} +1 -1
- package/src/console/ui/dist/assets/{panel-signals-9najbZY2.js → panel-signals-DzEa2Fnt.js} +1 -1
- package/src/console/ui/dist/assets/{panel-store-OHkP2pDp.js → panel-store-BJkbNgxx.js} +1 -1
- package/src/console/ui/dist/assets/{panel-traces-tn2JoY8U.js → panel-traces-BaRVO3gM.js} +1 -1
- package/src/console/ui/dist/assets/style-C8MxEWPd.css +3 -0
- package/src/console/ui/dist/favicon.svg +7 -0
- package/src/console/ui/dist/index.html +3 -2
- package/src/console/ui/shell/App.tsx +44 -4
- package/src/console/ui/shell/components/oke-logo.tsx +40 -0
- package/src/console/ui/shell/index.html +1 -0
- package/src/console/ui/shell/layout/Shell.tsx +2 -3
- package/src/console/ui/shell/panels/overview/OverviewPanel.tsx +8 -0
- package/src/console/ui/shell/public/favicon.svg +7 -0
- package/src/console/ui/shell/setup/Wizard.tsx +5 -2
- package/src/docker/compose.ts +45 -14
- package/src/docker/derive.ts +32 -10
- package/src/docker/docker.test.ts +28 -4
- package/src/docker/dockerfile.integration.test.ts +3 -1
- 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 +17 -8
- 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
- package/src/console/ui/dist/assets/style-Cnl7WLya.css +0 -3
package/src/cli/load-config.ts
CHANGED
|
@@ -3,9 +3,79 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
|
-
import type { OkeConfig } from "../config/index.ts";
|
|
6
|
+
import type { DriverRef, OkeConfig } from "../config/index.ts";
|
|
7
7
|
import type { Manifest } from "../manifest/types.ts";
|
|
8
8
|
|
|
9
|
+
/** Default image pins when `images` is omitted but prod drivers need containers. */
|
|
10
|
+
const DEFAULT_SQL_IMAGE = "postgres:18-alpine";
|
|
11
|
+
const DEFAULT_PGVECTOR_IMAGE = "pgvector/pgvector:pg17";
|
|
12
|
+
const DEFAULT_KV_IMAGE = "redis:8-alpine";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Extract protocol id from a driver ref.
|
|
16
|
+
*
|
|
17
|
+
* @param ref - String or `{ driver }` object
|
|
18
|
+
*/
|
|
19
|
+
function driverId(ref: DriverRef | undefined): string | undefined {
|
|
20
|
+
if (ref === undefined) return undefined;
|
|
21
|
+
return typeof ref === "string" ? ref : ref.driver;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
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.
|
|
37
|
+
*
|
|
38
|
+
* Used when `oke.config.ts` omits `images` but declares postgres/redis (etc.)
|
|
39
|
+
* so `oke dev -s` / `oke stack` / `oke docker` have something to run.
|
|
40
|
+
*
|
|
41
|
+
* @param config - Loaded config
|
|
42
|
+
*/
|
|
43
|
+
export function defaultImagesFromConfig(
|
|
44
|
+
config: OkeConfig,
|
|
45
|
+
): Readonly<Record<string, string>> {
|
|
46
|
+
const out: Record<string, string> = {};
|
|
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);
|
|
52
|
+
|
|
53
|
+
const needsSql =
|
|
54
|
+
sql === "postgres" ||
|
|
55
|
+
sql === "pgvector" ||
|
|
56
|
+
index === "pgvector" ||
|
|
57
|
+
signal === "postgres" ||
|
|
58
|
+
clock === "postgres" ||
|
|
59
|
+
(config.drivers?.prod ?? []).some(
|
|
60
|
+
(p) => p === "postgres" || p === "pgvector",
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (needsSql) {
|
|
64
|
+
out["store.sql"] =
|
|
65
|
+
sql === "pgvector" || index === "pgvector"
|
|
66
|
+
? DEFAULT_PGVECTOR_IMAGE
|
|
67
|
+
: DEFAULT_SQL_IMAGE;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const needsKv =
|
|
71
|
+
kv === "redis" || (config.drivers?.prod ?? []).includes("redis");
|
|
72
|
+
if (needsKv) {
|
|
73
|
+
out["store.kv"] = DEFAULT_KV_IMAGE;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return out;
|
|
77
|
+
}
|
|
78
|
+
|
|
9
79
|
/** Result of loading project config. */
|
|
10
80
|
export interface LoadedConfig {
|
|
11
81
|
readonly config: OkeConfig;
|
|
@@ -60,6 +130,9 @@ export async function loadManifest(path: string): Promise<Manifest> {
|
|
|
60
130
|
/**
|
|
61
131
|
* Resolve images map from config or Manifest.
|
|
62
132
|
*
|
|
133
|
+
* When neither declares `images`, derive defaults from prod driver protocols
|
|
134
|
+
* so `oke dev -s` works on scaffolded templates without an explicit pin map.
|
|
135
|
+
*
|
|
63
136
|
* @param config - Optional config
|
|
64
137
|
* @param manifest - Optional manifest
|
|
65
138
|
*/
|
|
@@ -67,5 +140,75 @@ export function resolveImages(
|
|
|
67
140
|
config?: OkeConfig,
|
|
68
141
|
manifest?: Manifest,
|
|
69
142
|
): Readonly<Record<string, string>> {
|
|
70
|
-
|
|
143
|
+
const explicit = config?.images ?? manifest?.images;
|
|
144
|
+
if (explicit && Object.keys(explicit).length > 0) return explicit;
|
|
145
|
+
if (config) return defaultImagesFromConfig(config);
|
|
146
|
+
return {};
|
|
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
|
+
);
|
|
71
214
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev port probing — prefer then +1 until free.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { findFreePort, resolveDevPorts } from "./ports.ts";
|
|
7
|
+
|
|
8
|
+
describe("findFreePort", () => {
|
|
9
|
+
test("returns preferred when free", async () => {
|
|
10
|
+
const port = await findFreePort(6530, new Set(), async () => false);
|
|
11
|
+
expect(port).toBe(6530);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("increments until free", async () => {
|
|
15
|
+
const busy = new Set([6530, 6531]);
|
|
16
|
+
const port = await findFreePort(
|
|
17
|
+
6530,
|
|
18
|
+
new Set(),
|
|
19
|
+
async (p) => busy.has(p),
|
|
20
|
+
);
|
|
21
|
+
expect(port).toBe(6532);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("skips occupied set even when probe says free", async () => {
|
|
25
|
+
const port = await findFreePort(
|
|
26
|
+
6530,
|
|
27
|
+
new Set([6530, 6531]),
|
|
28
|
+
async () => false,
|
|
29
|
+
);
|
|
30
|
+
expect(port).toBe(6532);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("passes through ephemeral 0", async () => {
|
|
34
|
+
expect(await findFreePort(0)).toBe(0);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("resolveDevPorts", () => {
|
|
39
|
+
test("keeps app · console · mcp distinct when preferred collide", async () => {
|
|
40
|
+
// Everything busy except 6531, 6534, 6536 — force increments.
|
|
41
|
+
const busy = new Set([6530, 6533, 6535]);
|
|
42
|
+
const ports = await resolveDevPorts(
|
|
43
|
+
{ app: 6530, console: 6533, mcp: 6535 },
|
|
44
|
+
async (p) => busy.has(p),
|
|
45
|
+
);
|
|
46
|
+
expect(ports.app).toBe(6531);
|
|
47
|
+
expect(ports.console).toBe(6534);
|
|
48
|
+
expect(ports.mcp).toBe(6536);
|
|
49
|
+
expect(new Set([ports.app, ports.console, ports.mcp]).size).toBe(3);
|
|
50
|
+
});
|
|
51
|
+
});
|
package/src/cli/ports.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-time port probing — Next.js-style prefer-then-increment.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createServer } from "node:net";
|
|
6
|
+
|
|
7
|
+
/** Max upward probes from a preferred port before failing. */
|
|
8
|
+
const MAX_PORT_ATTEMPTS = 100;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* True when something is already listening on `port` at 127.0.0.1.
|
|
12
|
+
*
|
|
13
|
+
* @param port - TCP port
|
|
14
|
+
*/
|
|
15
|
+
export function isPortInUse(port: number): Promise<boolean> {
|
|
16
|
+
return new Promise((resolvePromise) => {
|
|
17
|
+
const server = createServer();
|
|
18
|
+
server.once("error", (err: NodeJS.ErrnoException) => {
|
|
19
|
+
resolvePromise(err.code === "EADDRINUSE");
|
|
20
|
+
});
|
|
21
|
+
server.once("listening", () => {
|
|
22
|
+
server.close(() => resolvePromise(false));
|
|
23
|
+
});
|
|
24
|
+
server.listen(port, "127.0.0.1");
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Find the first free port at or above `preferred`, skipping occupied set.
|
|
30
|
+
*
|
|
31
|
+
* @param preferred - Starting port (e.g. 6530)
|
|
32
|
+
* @param occupied - Ports already claimed in this session
|
|
33
|
+
* @param probe - Injectable busy check (tests)
|
|
34
|
+
*/
|
|
35
|
+
export async function findFreePort(
|
|
36
|
+
preferred: number,
|
|
37
|
+
occupied: ReadonlySet<number> = new Set(),
|
|
38
|
+
probe: (port: number) => Promise<boolean> = isPortInUse,
|
|
39
|
+
): Promise<number> {
|
|
40
|
+
if (!Number.isFinite(preferred) || preferred < 0) {
|
|
41
|
+
throw new Error(`oke: invalid preferred port ${preferred}`);
|
|
42
|
+
}
|
|
43
|
+
// Ephemeral (0) — leave to the OS; no probing.
|
|
44
|
+
if (preferred === 0) return 0;
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < MAX_PORT_ATTEMPTS; i++) {
|
|
47
|
+
const port = preferred + i;
|
|
48
|
+
if (occupied.has(port)) continue;
|
|
49
|
+
if (!(await probe(port))) return port;
|
|
50
|
+
}
|
|
51
|
+
throw new Error(
|
|
52
|
+
`oke: no free port near ${preferred} after ${MAX_PORT_ATTEMPTS} attempts`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resolve distinct free ports for app · Console · MCP (dev only).
|
|
58
|
+
*
|
|
59
|
+
* @param preferred - Preferred ports
|
|
60
|
+
* @param probe - Injectable busy check (tests)
|
|
61
|
+
*/
|
|
62
|
+
export async function resolveDevPorts(
|
|
63
|
+
preferred: {
|
|
64
|
+
readonly app: number;
|
|
65
|
+
readonly console: number;
|
|
66
|
+
readonly mcp: number;
|
|
67
|
+
},
|
|
68
|
+
probe: (port: number) => Promise<boolean> = isPortInUse,
|
|
69
|
+
): Promise<{
|
|
70
|
+
readonly app: number;
|
|
71
|
+
readonly console: number;
|
|
72
|
+
readonly mcp: number;
|
|
73
|
+
}> {
|
|
74
|
+
const occupied = new Set<number>();
|
|
75
|
+
const app = await findFreePort(preferred.app, occupied, probe);
|
|
76
|
+
if (app !== 0) occupied.add(app);
|
|
77
|
+
const consolePort = await findFreePort(preferred.console, occupied, probe);
|
|
78
|
+
if (consolePort !== 0) occupied.add(consolePort);
|
|
79
|
+
const mcp = await findFreePort(preferred.mcp, occupied, probe);
|
|
80
|
+
return { app, console: consolePort, mcp };
|
|
81
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-level help — no Flags/JSON comment footers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { formatOkeHelp } from "./registry.ts";
|
|
7
|
+
|
|
8
|
+
describe("formatOkeHelp", () => {
|
|
9
|
+
test("lists commands without Flags/JSON commentary", () => {
|
|
10
|
+
const help = formatOkeHelp();
|
|
11
|
+
expect(help).toContain("oke — okengine CLI");
|
|
12
|
+
expect(help).toContain("Commands:");
|
|
13
|
+
expect(help).toContain("dev");
|
|
14
|
+
expect(help).not.toContain("Flags:");
|
|
15
|
+
expect(help).not.toContain("JSON:");
|
|
16
|
+
expect(help).not.toContain("Exit codes:");
|
|
17
|
+
});
|
|
18
|
+
});
|
package/src/cli/registry.ts
CHANGED
|
@@ -373,10 +373,6 @@ export function formatOkeHelp(): string {
|
|
|
373
373
|
}
|
|
374
374
|
}
|
|
375
375
|
lines.push("");
|
|
376
|
-
lines.push("Flags: long form is canonical; short form is convenience only.");
|
|
377
|
-
lines.push("JSON: doctor · stack · images list · gates list accept --json|-j");
|
|
378
|
-
lines.push(" (stdout = JSON only; hints/progress on stderr).");
|
|
379
|
-
lines.push("");
|
|
380
376
|
return `${lines.join("\n")}\n`;
|
|
381
377
|
}
|
|
382
378
|
|
|
@@ -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: { "store.sql": "postgres:
|
|
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
|
|
@@ -25,7 +25,7 @@ describe("oke safe-default overrides", () => {
|
|
|
25
25
|
const off = await runDockerDerive({
|
|
26
26
|
cwd: dir,
|
|
27
27
|
outDir: dir,
|
|
28
|
-
images: { "store.sql": "postgres:
|
|
28
|
+
images: { "store.sql": "postgres:18-alpine" },
|
|
29
29
|
dryRun: true,
|
|
30
30
|
write: () => {},
|
|
31
31
|
});
|
|
@@ -35,7 +35,7 @@ describe("oke safe-default overrides", () => {
|
|
|
35
35
|
const on = await runDockerDerive({
|
|
36
36
|
cwd: dir,
|
|
37
37
|
outDir: dir,
|
|
38
|
-
images: { "store.sql": "postgres:
|
|
38
|
+
images: { "store.sql": "postgres:18-alpine" },
|
|
39
39
|
prod: true,
|
|
40
40
|
dryRun: true,
|
|
41
41
|
write: () => {},
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Driver map resolution — including `stack` → prod fallback.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { resolveDriverId } from "./index.ts";
|
|
7
|
+
|
|
8
|
+
describe("resolveDriverId", () => {
|
|
9
|
+
test("reads the named env key", () => {
|
|
10
|
+
expect(
|
|
11
|
+
resolveDriverId({ dev: "sqlite", prod: "postgres" }, "dev"),
|
|
12
|
+
).toBe("sqlite");
|
|
13
|
+
expect(
|
|
14
|
+
resolveDriverId({ dev: "sqlite", prod: "postgres" }, "prod"),
|
|
15
|
+
).toBe("postgres");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("stack prefers stack, then prod, then dev", () => {
|
|
19
|
+
expect(
|
|
20
|
+
resolveDriverId(
|
|
21
|
+
{ dev: "sqlite", stack: "postgres", prod: "postgres" },
|
|
22
|
+
"stack",
|
|
23
|
+
),
|
|
24
|
+
).toBe("postgres");
|
|
25
|
+
expect(
|
|
26
|
+
resolveDriverId({ dev: "sqlite", prod: "postgres" }, "stack"),
|
|
27
|
+
).toBe("postgres");
|
|
28
|
+
expect(resolveDriverId({ dev: "sqlite" }, "stack")).toBe("sqlite");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -49,7 +49,8 @@ export function createConsoleApp(
|
|
|
49
49
|
options: CreateConsoleAppOptions = {},
|
|
50
50
|
): ConsoleAppHandle {
|
|
51
51
|
const state = createConsoleState(options);
|
|
52
|
-
|
|
52
|
+
// Spec §2.5 — claim prints only while setup is open (no operators yet).
|
|
53
|
+
if (!options.silentClaim && !state.setupClosed) {
|
|
53
54
|
printClaimCodeOnce(state.claim);
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -1771,6 +1771,7 @@ function createSetupClaim(state: ConsoleState) {
|
|
|
1771
1771
|
name: input.name,
|
|
1772
1772
|
password: input.password,
|
|
1773
1773
|
});
|
|
1774
|
+
state.persistOperator(op.id);
|
|
1774
1775
|
const issued = await issueOperatorSession(state, op.id);
|
|
1775
1776
|
fx.log.info("console.setup.claim", { operatorId: op.id });
|
|
1776
1777
|
return sessionPayload(op.id, op.email, op.name, issued);
|
|
@@ -2365,7 +2366,7 @@ async function issueOperatorSession(
|
|
|
2365
2366
|
): Promise<IssuedSession> {
|
|
2366
2367
|
const op = state.operators.operators.get(operatorId);
|
|
2367
2368
|
if (op) op.lastSeenAt = state.now();
|
|
2368
|
-
|
|
2369
|
+
const issued = await issueSession(
|
|
2369
2370
|
state.sessions,
|
|
2370
2371
|
{
|
|
2371
2372
|
secret: state.secret,
|
|
@@ -2378,6 +2379,8 @@ async function issueOperatorSession(
|
|
|
2378
2379
|
scopes: ["console:*"],
|
|
2379
2380
|
},
|
|
2380
2381
|
);
|
|
2382
|
+
state.persistSessions();
|
|
2383
|
+
return issued;
|
|
2381
2384
|
}
|
|
2382
2385
|
|
|
2383
2386
|
function sessionPayload(
|
|
@@ -71,6 +71,11 @@ export {
|
|
|
71
71
|
type ClaimCodeState,
|
|
72
72
|
type ClaimVerifyResult,
|
|
73
73
|
} from "./claim.ts";
|
|
74
|
+
export {
|
|
75
|
+
openConsolePersistence,
|
|
76
|
+
resolveConsoleSecret,
|
|
77
|
+
type ConsolePersistence,
|
|
78
|
+
} from "./operator-db.ts";
|
|
74
79
|
export {
|
|
75
80
|
createConsoleBindings,
|
|
76
81
|
} from "./flows.ts";
|