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
|
@@ -27,25 +27,25 @@ const credentials = {
|
|
|
27
27
|
} as const;
|
|
28
28
|
|
|
29
29
|
describe("oke docker CLI", () => {
|
|
30
|
-
test("writes Dockerfile and compose
|
|
30
|
+
test("writes Dockerfile and compose under docker/ by default", async () => {
|
|
31
31
|
const dir = await mkdtemp(join(tmpdir(), "oke-cli-docker-"));
|
|
32
32
|
const logs: string[] = [];
|
|
33
33
|
const { code, result } = await runDockerDerive({
|
|
34
34
|
cwd: dir,
|
|
35
|
-
outDir: dir,
|
|
36
35
|
images,
|
|
37
36
|
credentials,
|
|
38
37
|
write: (t) => logs.push(t),
|
|
39
38
|
});
|
|
40
39
|
expect(code).toBe(0);
|
|
41
40
|
expect(result).toBeDefined();
|
|
42
|
-
expect(await Bun.file(join(dir, "Dockerfile")).exists()).toBe(true);
|
|
43
|
-
expect(
|
|
44
|
-
|
|
45
|
-
);
|
|
46
|
-
const yml = await Bun.file(join(dir, "compose.store.sql.yml")).text();
|
|
41
|
+
expect(await Bun.file(join(dir, "docker/Dockerfile")).exists()).toBe(true);
|
|
42
|
+
expect(
|
|
43
|
+
await Bun.file(join(dir, "docker/compose.store.sql.yml")).exists(),
|
|
44
|
+
).toBe(true);
|
|
45
|
+
const yml = await Bun.file(join(dir, "docker/compose.store.sql.yml")).text();
|
|
47
46
|
expect(yml).not.toContain("test-password-not-in-yaml");
|
|
48
|
-
expect(
|
|
47
|
+
expect(yml).toContain("../.env.stack");
|
|
48
|
+
expect(logs.join("")).toContain("docker/compose.override.yml");
|
|
49
49
|
});
|
|
50
50
|
});
|
|
51
51
|
|
|
@@ -164,8 +164,10 @@ describe("oke dev --stack", () => {
|
|
|
164
164
|
expect(code).toBe(0);
|
|
165
165
|
expect(plan?.stackRoles).toEqual(["store.sql"]);
|
|
166
166
|
expect(plan?.composeFiles?.some((f) => f.includes("store.sql"))).toBe(true);
|
|
167
|
+
expect(plan?.composeFiles).toContain("compose.yml");
|
|
167
168
|
expect(plan?.stackEnv?.DATABASE_URL).toContain("postgres://");
|
|
168
169
|
expect(await Bun.file(join(dir, "compose.yml")).exists()).toBe(false);
|
|
170
|
+
expect(await Bun.file(join(dir, "docker/compose.yml")).exists()).toBe(false);
|
|
169
171
|
});
|
|
170
172
|
});
|
|
171
173
|
|
package/src/cli/docker.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { resolve } from "node:path";
|
|
6
6
|
import {
|
|
7
|
+
DEFAULT_DOCKER_DIR,
|
|
7
8
|
deriveInfrastructure,
|
|
8
9
|
writeDerivedFiles,
|
|
9
10
|
type DeriveOptions,
|
|
@@ -38,7 +39,15 @@ export async function runDockerDerive(
|
|
|
38
39
|
): Promise<{ readonly code: number; readonly result?: DeriveResult }> {
|
|
39
40
|
const write = options.write ?? ((t) => process.stdout.write(t));
|
|
40
41
|
const cwd = options.cwd ?? process.cwd();
|
|
41
|
-
const
|
|
42
|
+
const composeDirOpt = options.outDir ?? DEFAULT_DOCKER_DIR;
|
|
43
|
+
const outDir = resolve(cwd, composeDirOpt);
|
|
44
|
+
// Relative dir for env_file / build.context rewriting (`.` when out === cwd).
|
|
45
|
+
const composeDir =
|
|
46
|
+
outDir === cwd
|
|
47
|
+
? "."
|
|
48
|
+
: outDir.startsWith(`${cwd}/`) || outDir.startsWith(`${cwd}\\`)
|
|
49
|
+
? outDir.slice(cwd.length + 1)
|
|
50
|
+
: ".";
|
|
42
51
|
|
|
43
52
|
let images = options.images;
|
|
44
53
|
let app = "app";
|
|
@@ -70,6 +79,8 @@ export async function runDockerDerive(
|
|
|
70
79
|
app,
|
|
71
80
|
prod: options.prod ?? false,
|
|
72
81
|
outDir,
|
|
82
|
+
composeDir,
|
|
83
|
+
includeApp: true,
|
|
73
84
|
...(options.credentials ? { credentials: options.credentials } : {}),
|
|
74
85
|
});
|
|
75
86
|
|
|
@@ -77,13 +88,13 @@ export async function runDockerDerive(
|
|
|
77
88
|
await writeDerivedFiles(result, outDir, { writeStackEnv: false });
|
|
78
89
|
}
|
|
79
90
|
|
|
80
|
-
write(`oke docker: wrote ${result.files.length} file(s)
|
|
81
|
-
for (const f of result.files) write(` ${f.path}\n`);
|
|
91
|
+
write(`oke docker: wrote ${result.files.length} file(s) → ${composeDir}/\n`);
|
|
92
|
+
for (const f of result.files) write(` ${composeDir}/${f.path}\n`);
|
|
82
93
|
write(
|
|
83
|
-
`compose merge order:\n${result.composeFiles.map((f) => ` -f ${f}`).join("\n")}\n`,
|
|
94
|
+
`compose merge order (cwd ${composeDir}/):\n${result.composeFiles.map((f) => ` -f ${f}`).join("\n")}\n`,
|
|
84
95
|
);
|
|
85
96
|
write(
|
|
86
|
-
|
|
97
|
+
`(${composeDir}/compose.override.yml is layer 4 — user-owned, never written by oke)\n`,
|
|
87
98
|
);
|
|
88
99
|
return { code: 0, result };
|
|
89
100
|
} catch (err) {
|
|
@@ -109,9 +120,9 @@ export async function dockerCli(args: readonly string[]): Promise<number> {
|
|
|
109
120
|
else if (a === "--config" || a === "-c") configPath = args[++i];
|
|
110
121
|
else if (a === "--manifest" || a === "-m") manifestPath = args[++i];
|
|
111
122
|
else if (a === "--help" || a === "-h") {
|
|
112
|
-
console.log(`oke docker [--prod|-p] [--out|-o
|
|
123
|
+
console.log(`oke docker [--prod|-p] [--out|-o docker] [--config|-c oke.config.ts]
|
|
113
124
|
|
|
114
|
-
Derive Dockerfile + compose.<role>.yml
|
|
125
|
+
Derive Dockerfile + compose.<role>.yml under docker/ (default).
|
|
115
126
|
--prod is opt-in (never the default). Credentials are never written into YAML.
|
|
116
127
|
`);
|
|
117
128
|
return 0;
|
package/src/cli/doctor.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* `oke doctor` — verify secrets, ports, drivers, tenancy, schema drift.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { createServer } from "node:net";
|
|
6
5
|
import { resolve } from "node:path";
|
|
7
6
|
import type { Manifest } from "../manifest/types.ts";
|
|
8
7
|
import { APP_PORT, CONSOLE_PORT, MCP_PORT } from "../runtime/types.ts";
|
|
@@ -10,8 +9,11 @@ import { hasFlag, wantsJson } from "./args.ts";
|
|
|
10
9
|
import { checkManifestPiiAsks } from "./doctor-pii.ts";
|
|
11
10
|
import { EXIT_OK, EXIT_RUNTIME } from "./exit.ts";
|
|
12
11
|
import { loadManifest } from "./load-config.ts";
|
|
12
|
+
import { isPortInUse } from "./ports.ts";
|
|
13
13
|
import { schemaFingerprint, readSchemaFingerprint } from "./schema.ts";
|
|
14
14
|
|
|
15
|
+
export { isPortInUse } from "./ports.ts";
|
|
16
|
+
|
|
15
17
|
/** One doctor finding. */
|
|
16
18
|
export interface DoctorFinding {
|
|
17
19
|
readonly code:
|
|
@@ -204,20 +206,3 @@ Verify secrets, ports, schema drift, and PII→model egress before serving.
|
|
|
204
206
|
return code;
|
|
205
207
|
}
|
|
206
208
|
|
|
207
|
-
/**
|
|
208
|
-
* True when something is already listening on `port`.
|
|
209
|
-
*
|
|
210
|
-
* @param port - TCP port
|
|
211
|
-
*/
|
|
212
|
-
export function isPortInUse(port: number): Promise<boolean> {
|
|
213
|
-
return new Promise((resolvePromise) => {
|
|
214
|
-
const server = createServer();
|
|
215
|
-
server.once("error", (err: NodeJS.ErrnoException) => {
|
|
216
|
-
resolvePromise(err.code === "EADDRINUSE");
|
|
217
|
-
});
|
|
218
|
-
server.once("listening", () => {
|
|
219
|
-
server.close(() => resolvePromise(false));
|
|
220
|
-
});
|
|
221
|
-
server.listen(port, "127.0.0.1");
|
|
222
|
-
});
|
|
223
|
-
}
|
|
@@ -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
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -90,7 +90,10 @@ if (cmd === "completion") {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
if (cmd === undefined || cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
93
|
-
|
|
93
|
+
// Bare `oke` — commands only. Exit-code table only on explicit --help.
|
|
94
|
+
const help =
|
|
95
|
+
cmd === undefined ? formatOkeHelp() : `${formatOkeHelp()}${EXIT_CODE_HELP}`;
|
|
96
|
+
console.log(help);
|
|
94
97
|
process.exit(cmd ? EXIT_OK : EXIT_USAGE);
|
|
95
98
|
}
|
|
96
99
|
|
package/src/cli/json-out.test.ts
CHANGED
|
@@ -42,7 +42,7 @@ describe("oke --json stdout discipline", () => {
|
|
|
42
42
|
test("stack --json is valid JSON only on stdout", async () => {
|
|
43
43
|
let out = "";
|
|
44
44
|
const code = await runStackPreview({
|
|
45
|
-
images: { "store.sql": "postgres:
|
|
45
|
+
images: { "store.sql": "postgres:18-alpine" },
|
|
46
46
|
json: true,
|
|
47
47
|
write: (t) => {
|
|
48
48
|
out += t;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default image pins derived from prod drivers.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import {
|
|
7
|
+
defaultImagesFromConfig,
|
|
8
|
+
resolveImages,
|
|
9
|
+
stackDevDriverMismatches,
|
|
10
|
+
} from "./load-config.ts";
|
|
11
|
+
|
|
12
|
+
describe("defaultImagesFromConfig", () => {
|
|
13
|
+
test("maps postgres + redis prod drivers", () => {
|
|
14
|
+
const images = defaultImagesFromConfig({
|
|
15
|
+
drivers: {
|
|
16
|
+
store: {
|
|
17
|
+
sql: { prod: "postgres" },
|
|
18
|
+
kv: { prod: "redis" },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
expect(images).toEqual({
|
|
23
|
+
"store.sql": "postgres:18-alpine",
|
|
24
|
+
"store.kv": "redis:8-alpine",
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("prefers pgvector image when index uses pgvector", () => {
|
|
29
|
+
const images = defaultImagesFromConfig({
|
|
30
|
+
drivers: {
|
|
31
|
+
store: {
|
|
32
|
+
sql: { prod: "postgres" },
|
|
33
|
+
index: { prod: "pgvector" },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
expect(images["store.sql"]).toBe("pgvector/pgvector:pg17");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("returns empty when no container drivers", () => {
|
|
41
|
+
expect(defaultImagesFromConfig({})).toEqual({});
|
|
42
|
+
expect(
|
|
43
|
+
defaultImagesFromConfig({
|
|
44
|
+
drivers: { store: { sql: { prod: "sqlite" } } },
|
|
45
|
+
}),
|
|
46
|
+
).toEqual({});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("resolveImages", () => {
|
|
51
|
+
test("explicit images win over defaults", () => {
|
|
52
|
+
const images = resolveImages({
|
|
53
|
+
drivers: {
|
|
54
|
+
store: {
|
|
55
|
+
sql: { prod: "postgres" },
|
|
56
|
+
kv: { prod: "redis" },
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
images: { "store.sql": "pgvector/pgvector:pg17" },
|
|
60
|
+
});
|
|
61
|
+
expect(images).toEqual({ "store.sql": "pgvector/pgvector:pg17" });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("falls back to driver defaults when images omitted", () => {
|
|
65
|
+
const images = resolveImages({
|
|
66
|
+
drivers: {
|
|
67
|
+
store: {
|
|
68
|
+
sql: { prod: "postgres" },
|
|
69
|
+
kv: { prod: "redis" },
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
expect(images["store.sql"]).toBe("postgres:18-alpine");
|
|
74
|
+
expect(images["store.kv"]).toBe("redis:8-alpine");
|
|
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
|
+
});
|