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
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-project stack identity.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import {
|
|
7
|
+
hostPortForInstance,
|
|
8
|
+
parseStackCredentials,
|
|
9
|
+
stackAppSlug,
|
|
10
|
+
stackInstanceId,
|
|
11
|
+
} from "./stack-id.ts";
|
|
12
|
+
import { deriveInfrastructure } from "./derive.ts";
|
|
13
|
+
|
|
14
|
+
describe("stackInstanceId", () => {
|
|
15
|
+
test("is stable for the same cwd and differs across paths", () => {
|
|
16
|
+
const a = stackInstanceId("/tmp/oke-project-a");
|
|
17
|
+
const b = stackInstanceId("/tmp/oke-project-a");
|
|
18
|
+
const c = stackInstanceId("/tmp/oke-project-b");
|
|
19
|
+
expect(a).toBe(b);
|
|
20
|
+
expect(a).toHaveLength(6);
|
|
21
|
+
expect(a).not.toBe(c);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("stackAppSlug is dev-<id>", () => {
|
|
25
|
+
const id = stackInstanceId("/tmp/oke-x");
|
|
26
|
+
expect(stackAppSlug("/tmp/oke-x")).toBe(`dev-${id}`);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("hostPortForInstance", () => {
|
|
31
|
+
test("offsets sql and kv into separate ranges", () => {
|
|
32
|
+
const id = "abcd12";
|
|
33
|
+
const sql = hostPortForInstance("store.sql", 5432, id);
|
|
34
|
+
const kv = hostPortForInstance("store.kv", 6379, id);
|
|
35
|
+
expect(sql).toBeGreaterThanOrEqual(15_000);
|
|
36
|
+
expect(sql).toBeLessThan(16_000);
|
|
37
|
+
expect(kv).toBeGreaterThanOrEqual(16_000);
|
|
38
|
+
expect(kv).toBeLessThan(17_000);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("parseStackCredentials", () => {
|
|
43
|
+
test("reads OKE_* credential keys", () => {
|
|
44
|
+
const text = `
|
|
45
|
+
OKE_STORE_SQL_USER=oke
|
|
46
|
+
OKE_STORE_SQL_PASSWORD=s3cret
|
|
47
|
+
OKE_STORE_SQL_DB=oke
|
|
48
|
+
OKE_STORE_KV_USER=oke
|
|
49
|
+
OKE_STORE_KV_PASSWORD=kvpass
|
|
50
|
+
OKE_STORE_KV_DB=oke
|
|
51
|
+
`;
|
|
52
|
+
const creds = parseStackCredentials(text, ["store.sql", "store.kv"]);
|
|
53
|
+
expect(creds["store.sql"]?.password).toBe("s3cret");
|
|
54
|
+
expect(creds["store.kv"]?.password).toBe("kvpass");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("deriveInfrastructure instanceId", () => {
|
|
59
|
+
test("unique app name and host ports land in stackEnv URLs", () => {
|
|
60
|
+
const result = deriveInfrastructure({
|
|
61
|
+
images: {
|
|
62
|
+
"store.sql": "postgres:18-alpine",
|
|
63
|
+
"store.kv": "redis:8-alpine",
|
|
64
|
+
},
|
|
65
|
+
app: "dev-abcdef",
|
|
66
|
+
instanceId: "abcdef",
|
|
67
|
+
includeApp: false,
|
|
68
|
+
credentials: {
|
|
69
|
+
"store.sql": {
|
|
70
|
+
user: "oke",
|
|
71
|
+
password: "stack-id-test-sql-password",
|
|
72
|
+
database: "oke",
|
|
73
|
+
},
|
|
74
|
+
"store.kv": {
|
|
75
|
+
user: "oke",
|
|
76
|
+
password: "stack-id-test-kv-password",
|
|
77
|
+
database: "oke",
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
const base = result.files.find((f) => f.path === "compose.yml")!.content;
|
|
82
|
+
expect(base).toContain("oke-dev-abcdef");
|
|
83
|
+
expect(result.stackEnv.DATABASE_URL).toMatch(/:15\d{3}\//);
|
|
84
|
+
expect(result.stackEnv.REDIS_URL).toMatch(/:16\d{3}/);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-project local stack identity — unique compose project + host ports.
|
|
3
|
+
*
|
|
4
|
+
* `oke dev --stack` must not share one `oke-dev` Postgres across every app on
|
|
5
|
+
* the machine. Identity is a stable short hash of the project cwd.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import type { ServiceCredentials } from "./types.ts";
|
|
11
|
+
import { defaultHostPort, envPrefix } from "./helpers.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Stable 6-hex id for a project directory (local stacks only).
|
|
15
|
+
*
|
|
16
|
+
* @param cwd - Project root
|
|
17
|
+
*/
|
|
18
|
+
export function stackInstanceId(cwd: string): string {
|
|
19
|
+
return createHash("sha256")
|
|
20
|
+
.update(resolve(cwd))
|
|
21
|
+
.digest("hex")
|
|
22
|
+
.slice(0, 6);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Compose project / app slug: `dev-<id>` → Docker name `oke-dev-<id>`.
|
|
27
|
+
*
|
|
28
|
+
* @param cwd - Project root
|
|
29
|
+
*/
|
|
30
|
+
export function stackAppSlug(cwd: string): string {
|
|
31
|
+
return `dev-${stackInstanceId(cwd)}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Host port for a role, offset by instance id so two stacks can run at once.
|
|
36
|
+
*
|
|
37
|
+
* @param role - Role key
|
|
38
|
+
* @param containerPort - Container listen port
|
|
39
|
+
* @param instanceId - 6-hex stack id
|
|
40
|
+
*/
|
|
41
|
+
export function hostPortForInstance(
|
|
42
|
+
role: string,
|
|
43
|
+
containerPort: number,
|
|
44
|
+
instanceId: string,
|
|
45
|
+
): number {
|
|
46
|
+
const n = Number.parseInt(instanceId.slice(0, 4), 16) % 1000;
|
|
47
|
+
if (role === "store.sql") return 15_000 + n;
|
|
48
|
+
if (role === "store.kv") return 16_000 + n;
|
|
49
|
+
if (role === "signal") return 17_000 + n;
|
|
50
|
+
return defaultHostPort(role, containerPort) + n;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parse role credentials from an existing `.env.stack` body (reuse on restart).
|
|
55
|
+
*
|
|
56
|
+
* @param text - Dotenv contents
|
|
57
|
+
* @param roles - Roles to look up
|
|
58
|
+
*/
|
|
59
|
+
export function parseStackCredentials(
|
|
60
|
+
text: string,
|
|
61
|
+
roles: readonly string[],
|
|
62
|
+
): Record<string, ServiceCredentials> {
|
|
63
|
+
const map = new Map<string, string>();
|
|
64
|
+
for (const line of text.split("\n")) {
|
|
65
|
+
const trimmed = line.trim();
|
|
66
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
67
|
+
const eq = trimmed.indexOf("=");
|
|
68
|
+
if (eq <= 0) continue;
|
|
69
|
+
const key = trimmed.slice(0, eq);
|
|
70
|
+
let value = trimmed.slice(eq + 1);
|
|
71
|
+
if (
|
|
72
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
73
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
74
|
+
) {
|
|
75
|
+
value = value.slice(1, -1);
|
|
76
|
+
}
|
|
77
|
+
map.set(key, value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const out: Record<string, ServiceCredentials> = {};
|
|
81
|
+
for (const role of roles) {
|
|
82
|
+
const prefix = envPrefix(role);
|
|
83
|
+
const user = map.get(`${prefix}_USER`);
|
|
84
|
+
const password = map.get(`${prefix}_PASSWORD`);
|
|
85
|
+
const database = map.get(`${prefix}_DB`);
|
|
86
|
+
if (user && password && database) {
|
|
87
|
+
out[role] = { user, password, database };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Load credentials from project `.env.stack` when present.
|
|
95
|
+
*
|
|
96
|
+
* @param cwd - Project root
|
|
97
|
+
* @param roles - Image roles
|
|
98
|
+
*/
|
|
99
|
+
export async function loadExistingStackCredentials(
|
|
100
|
+
cwd: string,
|
|
101
|
+
roles: readonly string[],
|
|
102
|
+
): Promise<Readonly<Record<string, ServiceCredentials>> | undefined> {
|
|
103
|
+
const path = resolve(cwd, ".env.stack");
|
|
104
|
+
const file = Bun.file(path);
|
|
105
|
+
if (!(await file.exists())) return undefined;
|
|
106
|
+
const parsed = parseStackCredentials(await file.text(), roles);
|
|
107
|
+
return Object.keys(parsed).length > 0 ? parsed : undefined;
|
|
108
|
+
}
|
|
@@ -32,10 +32,11 @@ describe("oke dev --stack postgres integration", () => {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
const dir = await mkdtemp(join(tmpdir(), "oke-stack-pg-"));
|
|
35
|
+
const dockerDir = join(dir, "docker");
|
|
35
36
|
const project = `oke-pg-${Date.now()}`;
|
|
36
37
|
try {
|
|
37
38
|
const derived = deriveInfrastructure({
|
|
38
|
-
images: { "store.sql": "postgres:
|
|
39
|
+
images: { "store.sql": "postgres:18-alpine" },
|
|
39
40
|
credentials: {
|
|
40
41
|
"store.sql": {
|
|
41
42
|
user: "oke",
|
|
@@ -45,11 +46,16 @@ describe("oke dev --stack postgres integration", () => {
|
|
|
45
46
|
},
|
|
46
47
|
app: "stacktest",
|
|
47
48
|
host: "127.0.0.1",
|
|
49
|
+
includeApp: false,
|
|
50
|
+
composeDir: "docker",
|
|
51
|
+
});
|
|
52
|
+
await writeDerivedFiles(derived, dockerDir, {
|
|
53
|
+
writeStackEnv: true,
|
|
54
|
+
stackEnvDir: dir,
|
|
48
55
|
});
|
|
49
|
-
await writeDerivedFiles(derived, dir, { writeStackEnv: true });
|
|
50
56
|
|
|
51
|
-
//
|
|
52
|
-
const composeFiles = ["compose.store.sql.yml"];
|
|
57
|
+
// Infra-only: network + role compose (no app build).
|
|
58
|
+
const composeFiles = ["compose.yml", "compose.store.sql.yml"];
|
|
53
59
|
const up = Bun.spawn(
|
|
54
60
|
[
|
|
55
61
|
"docker",
|
|
@@ -61,7 +67,7 @@ describe("oke dev --stack postgres integration", () => {
|
|
|
61
67
|
"-d",
|
|
62
68
|
],
|
|
63
69
|
{
|
|
64
|
-
cwd:
|
|
70
|
+
cwd: dockerDir,
|
|
65
71
|
stdout: "pipe",
|
|
66
72
|
stderr: "pipe",
|
|
67
73
|
env: {
|
|
@@ -92,7 +98,7 @@ describe("oke dev --stack postgres integration", () => {
|
|
|
92
98
|
"--format",
|
|
93
99
|
"json",
|
|
94
100
|
],
|
|
95
|
-
{ cwd:
|
|
101
|
+
{ cwd: dockerDir, stdout: "pipe", stderr: "pipe" },
|
|
96
102
|
);
|
|
97
103
|
const text = await new Response(ps.stdout).text();
|
|
98
104
|
await ps.exited;
|
|
@@ -115,11 +121,12 @@ describe("oke dev --stack postgres integration", () => {
|
|
|
115
121
|
}
|
|
116
122
|
|
|
117
123
|
// Prove credentials live in .env.stack, not YAML.
|
|
118
|
-
const yml = await Bun.file(join(
|
|
124
|
+
const yml = await Bun.file(join(dockerDir, "compose.store.sql.yml")).text();
|
|
119
125
|
expect(yml).not.toContain("stack-integration-pass");
|
|
120
126
|
expect(formatStackEnv(derived.stackEnv)).toContain(
|
|
121
127
|
"stack-integration-pass",
|
|
122
128
|
);
|
|
129
|
+
expect(await Bun.file(join(dir, ".env.stack")).exists()).toBe(true);
|
|
123
130
|
} finally {
|
|
124
131
|
await Bun.spawn(
|
|
125
132
|
[
|
|
@@ -128,11 +135,13 @@ describe("oke dev --stack postgres integration", () => {
|
|
|
128
135
|
"-p",
|
|
129
136
|
project,
|
|
130
137
|
"-f",
|
|
138
|
+
"compose.yml",
|
|
139
|
+
"-f",
|
|
131
140
|
"compose.store.sql.yml",
|
|
132
141
|
"down",
|
|
133
142
|
"-v",
|
|
134
143
|
],
|
|
135
|
-
{ cwd:
|
|
144
|
+
{ cwd: dockerDir, stdout: "pipe", stderr: "pipe" },
|
|
136
145
|
).exited.catch(() => {});
|
|
137
146
|
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
138
147
|
}
|
package/src/docker/types.ts
CHANGED
|
@@ -100,7 +100,18 @@ export interface DeriveOptions {
|
|
|
100
100
|
readonly appPort?: number;
|
|
101
101
|
/** When true, emit prod overlays (limits, secret refs, deploy.replicas). */
|
|
102
102
|
readonly prod?: boolean;
|
|
103
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* Include the `app` service in `compose.yml` (default true).
|
|
105
|
+
* `oke dev --stack` sets false — host Bun runs the app; Docker is infra only.
|
|
106
|
+
*/
|
|
107
|
+
readonly includeApp?: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* Compose artefact directory relative to the project root (default `docker`).
|
|
110
|
+
* Controls generated `env_file` / `build.context` paths (e.g. `../.env.stack`).
|
|
111
|
+
* Pass `"."` for legacy root-level layout.
|
|
112
|
+
*/
|
|
113
|
+
readonly composeDir?: string;
|
|
114
|
+
/** Output directory (default `docker`). */
|
|
104
115
|
readonly outDir?: string;
|
|
105
116
|
/** Inject credentials (tests). Defaults to generated randoms. */
|
|
106
117
|
readonly credentials?: Readonly<Record<string, ServiceCredentials>>;
|
|
@@ -108,8 +119,16 @@ export interface DeriveOptions {
|
|
|
108
119
|
readonly host?: string;
|
|
109
120
|
/** Extra image recipes (plugins). */
|
|
110
121
|
readonly recipes?: readonly ImageRecipe[];
|
|
122
|
+
/**
|
|
123
|
+
* Local stack instance id (6-hex). When set, host ports are offset so two
|
|
124
|
+
* `oke dev -s` projects do not share one Postgres on `:5432`.
|
|
125
|
+
*/
|
|
126
|
+
readonly instanceId?: string;
|
|
111
127
|
}
|
|
112
128
|
|
|
129
|
+
/** Default relative directory for generated Docker / compose artefacts. */
|
|
130
|
+
export const DEFAULT_DOCKER_DIR = "docker";
|
|
131
|
+
|
|
113
132
|
/** One generated file. */
|
|
114
133
|
export interface GeneratedFile {
|
|
115
134
|
/** Relative path under `outDir`. */
|
package/src/kernel/app.ts
CHANGED
|
@@ -45,6 +45,11 @@ import {
|
|
|
45
45
|
type FxOperator,
|
|
46
46
|
type NamedRef,
|
|
47
47
|
} from "./fx.ts";
|
|
48
|
+
import {
|
|
49
|
+
currentDevSurface,
|
|
50
|
+
logDevRequest,
|
|
51
|
+
shouldLogDevRequests,
|
|
52
|
+
} from "../runtime/dev-request-log.ts";
|
|
48
53
|
import {
|
|
49
54
|
mergeHooks,
|
|
50
55
|
runPipeline,
|
|
@@ -126,6 +131,11 @@ export interface OkeOptions {
|
|
|
126
131
|
readonly archiveInputFields?: readonly string[];
|
|
127
132
|
/** Active environment for {@link OkeApp.boot} (defaults to `dev`). */
|
|
128
133
|
readonly env?: BootOptions["env"];
|
|
134
|
+
/**
|
|
135
|
+
* Local-server mode (`oke dev -s` / `OKE_STACK=1`) — force the `stack`
|
|
136
|
+
* driver profile at boot.
|
|
137
|
+
*/
|
|
138
|
+
readonly stack?: BootOptions["stack"];
|
|
129
139
|
/** Optional `oke.config.ts` document consumed at boot. */
|
|
130
140
|
readonly config?: BootOptions["config"];
|
|
131
141
|
/** Pre-built element runtimes (skip construction at boot when present). */
|
|
@@ -483,6 +493,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
483
493
|
bootEnv = overrides?.env ?? options.env ?? "dev";
|
|
484
494
|
const merged: BootOptions = {
|
|
485
495
|
env: bootEnv,
|
|
496
|
+
stack: overrides?.stack ?? options.stack,
|
|
486
497
|
config: overrides?.config ?? options.config,
|
|
487
498
|
elements: overrides?.elements ?? options.elements,
|
|
488
499
|
secrets: overrides?.secrets ?? options.secrets,
|
|
@@ -892,13 +903,31 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
892
903
|
},
|
|
893
904
|
execute,
|
|
894
905
|
async fetch(request) {
|
|
906
|
+
const started = performance.now();
|
|
907
|
+
let flowLabel: string | undefined;
|
|
895
908
|
const url = new URL(request.url);
|
|
896
909
|
const method = request.method.toUpperCase();
|
|
897
910
|
|
|
911
|
+
const respond = (response: Response): Response => {
|
|
912
|
+
if (shouldLogDevRequests()) {
|
|
913
|
+
logDevRequest({
|
|
914
|
+
surface: currentDevSurface(),
|
|
915
|
+
method,
|
|
916
|
+
path: url.pathname,
|
|
917
|
+
flow: flowLabel,
|
|
918
|
+
status: response.status,
|
|
919
|
+
ms: Math.round(performance.now() - started),
|
|
920
|
+
});
|
|
921
|
+
}
|
|
922
|
+
return response;
|
|
923
|
+
};
|
|
924
|
+
|
|
898
925
|
if (method === "GET" && url.pathname === "/_oke/client.json") {
|
|
899
|
-
return
|
|
900
|
-
|
|
901
|
-
|
|
926
|
+
return respond(
|
|
927
|
+
new Response(JSON.stringify(routes), {
|
|
928
|
+
headers: { "content-type": "application/json" },
|
|
929
|
+
}),
|
|
930
|
+
);
|
|
902
931
|
}
|
|
903
932
|
|
|
904
933
|
if (method === "POST" && url.pathname.startsWith("/_oke/")) {
|
|
@@ -910,8 +939,9 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
910
939
|
const target =
|
|
911
940
|
flowsByName.get(`${unit}.${flowName}`) ?? flowsByName.get(flowName);
|
|
912
941
|
if (!target) {
|
|
913
|
-
return new Response("Not Found", { status: 404 });
|
|
942
|
+
return respond(new Response("Not Found", { status: 404 }));
|
|
914
943
|
}
|
|
944
|
+
flowLabel = target.name;
|
|
915
945
|
let internalInput: unknown;
|
|
916
946
|
try {
|
|
917
947
|
internalInput = await request.json();
|
|
@@ -924,15 +954,16 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
924
954
|
{ kind: "internal" } satisfies InternalTrigger,
|
|
925
955
|
{ request },
|
|
926
956
|
);
|
|
927
|
-
return encodeExecuteResult(internalResult);
|
|
957
|
+
return respond(encodeExecuteResult(internalResult));
|
|
928
958
|
}
|
|
929
959
|
}
|
|
930
960
|
|
|
931
961
|
const matched = router.match(method, url.pathname);
|
|
932
962
|
if (!matched) {
|
|
933
|
-
return new Response("Not Found", { status: 404 });
|
|
963
|
+
return respond(new Response("Not Found", { status: 404 }));
|
|
934
964
|
}
|
|
935
965
|
const { value: binding, params } = matched;
|
|
966
|
+
flowLabel = binding.flow.name;
|
|
936
967
|
|
|
937
968
|
let route = compiled.get(binding);
|
|
938
969
|
if (!route && binding.trigger.kind === "http") {
|
|
@@ -946,7 +977,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
946
977
|
if (route) {
|
|
947
978
|
const parsed = await route.parseValidate(request, params);
|
|
948
979
|
if (!parsed.ok) {
|
|
949
|
-
return encodeFailure(parsed.failure);
|
|
980
|
+
return respond(encodeFailure(parsed.failure));
|
|
950
981
|
}
|
|
951
982
|
input = parsed.input;
|
|
952
983
|
validated = true;
|
|
@@ -962,7 +993,7 @@ export function oke(options: OkeOptions): OkeApp {
|
|
|
962
993
|
{ request, params, validated },
|
|
963
994
|
);
|
|
964
995
|
|
|
965
|
-
return encodeExecuteResult(result);
|
|
996
|
+
return respond(encodeExecuteResult(result));
|
|
966
997
|
},
|
|
967
998
|
async dispatchSignal(signal, payload) {
|
|
968
999
|
const name = resolveName(signal);
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store binder — stack profile (+ env overrides).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
6
|
+
import { resolveKvDriverId, resolveSqlDriverId } from "./store.ts";
|
|
7
|
+
|
|
8
|
+
describe("bindStore driver resolution", () => {
|
|
9
|
+
const prev = {
|
|
10
|
+
stack: process.env.OKE_STACK,
|
|
11
|
+
sql: process.env.OKE_SQL_DRIVER,
|
|
12
|
+
kv: process.env.OKE_KV_DRIVER,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
if (prev.stack === undefined) delete process.env.OKE_STACK;
|
|
17
|
+
else process.env.OKE_STACK = prev.stack;
|
|
18
|
+
if (prev.sql === undefined) delete process.env.OKE_SQL_DRIVER;
|
|
19
|
+
else process.env.OKE_SQL_DRIVER = prev.sql;
|
|
20
|
+
if (prev.kv === undefined) delete process.env.OKE_KV_DRIVER;
|
|
21
|
+
else process.env.OKE_KV_DRIVER = prev.kv;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("dev env keeps sqlite / memory from config", () => {
|
|
25
|
+
const options = {
|
|
26
|
+
config: {
|
|
27
|
+
drivers: {
|
|
28
|
+
store: {
|
|
29
|
+
sql: { dev: "sqlite", stack: "postgres", prod: "postgres" },
|
|
30
|
+
kv: { dev: "memory", stack: "redis", prod: "redis" },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
expect(resolveSqlDriverId(options, "dev", false)).toBe("sqlite");
|
|
36
|
+
expect(resolveKvDriverId(options, "dev", false)).toBe("memory");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("stack env uses stack profile (falls back to prod)", () => {
|
|
40
|
+
const options = {
|
|
41
|
+
config: {
|
|
42
|
+
drivers: {
|
|
43
|
+
store: {
|
|
44
|
+
sql: { dev: "sqlite", stack: "postgres", prod: "postgres" },
|
|
45
|
+
kv: { dev: "memory", prod: "redis" },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
expect(resolveSqlDriverId(options, "stack", true)).toBe("postgres");
|
|
51
|
+
expect(resolveKvDriverId(options, "stack", true)).toBe("redis");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("stack mode honours OKE_*_DRIVER overrides", () => {
|
|
55
|
+
process.env.OKE_SQL_DRIVER = "postgres";
|
|
56
|
+
process.env.OKE_KV_DRIVER = "redis";
|
|
57
|
+
expect(resolveSqlDriverId({}, "stack", true)).toBe("postgres");
|
|
58
|
+
expect(resolveKvDriverId({}, "stack", true)).toBe("redis");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -2,42 +2,70 @@
|
|
|
2
2
|
* Lazy store binder — loaded only when Store is declared.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import {
|
|
6
|
+
resolveDriverId,
|
|
7
|
+
type ConfigEnv,
|
|
8
|
+
} from "../../config/index.ts";
|
|
5
9
|
import { memoryDrivers } from "../../drivers/memory.ts";
|
|
10
|
+
import { postgresDriver } from "../../drivers/postgres.ts";
|
|
11
|
+
import { redisDriver } from "../../drivers/redis.ts";
|
|
12
|
+
import { sqliteDriver } from "../../drivers/sqlite.ts";
|
|
13
|
+
import type { KvDriver, SqlDriver } from "../../drivers/types.ts";
|
|
6
14
|
import {
|
|
7
15
|
createStoreRuntime,
|
|
8
16
|
type StoreRuntime,
|
|
9
17
|
} from "../../elements/store.ts";
|
|
10
|
-
import {
|
|
11
|
-
resolveDriverId,
|
|
12
|
-
type ConfigEnv,
|
|
13
|
-
} from "../../config/index.ts";
|
|
18
|
+
import type { StoreDecl } from "../../elements/store/declare.ts";
|
|
14
19
|
import type { BootOptions } from "../boot.ts"; // type-only — no cycle at runtime
|
|
15
20
|
|
|
16
21
|
/**
|
|
17
22
|
* Construct a Store runtime and register facet declarations.
|
|
18
23
|
*
|
|
24
|
+
* In stack mode (`env === "stack"` / `OKE_STACK=1`), SQL/KV resolve from the
|
|
25
|
+
* `stack` driver map (falling back to `prod`) and URLs from `.env.stack`.
|
|
26
|
+
*
|
|
19
27
|
* @param options - Boot options
|
|
20
28
|
* @param env - Active environment
|
|
21
29
|
* @param now - Clock
|
|
30
|
+
* @param stack - Prefer compose URLs when opening postgres/redis
|
|
22
31
|
*/
|
|
23
32
|
export function bindStore(
|
|
24
33
|
options: BootOptions,
|
|
25
34
|
env: ConfigEnv,
|
|
26
35
|
now: () => number,
|
|
36
|
+
stack = false,
|
|
27
37
|
): StoreRuntime {
|
|
28
|
-
const sqlId =
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
const sqlId = resolveSqlDriverId(options, env, stack);
|
|
39
|
+
const kvId = resolveKvDriverId(options, env, stack);
|
|
40
|
+
const sqlUrl = sqlUrlFor(sqlId, stack);
|
|
41
|
+
const kvUrl = kvUrlFor(kvId, stack);
|
|
42
|
+
|
|
43
|
+
const sqlBindings: Record<
|
|
44
|
+
string,
|
|
45
|
+
{ name: string; primary: { url: string } }
|
|
46
|
+
> = {};
|
|
47
|
+
const kvBindings: Record<string, { url?: string }> = {};
|
|
48
|
+
|
|
49
|
+
for (const decl of options.stores ?? []) {
|
|
50
|
+
if (isSqlDecl(decl)) {
|
|
51
|
+
sqlBindings[decl.name] = {
|
|
52
|
+
name: decl.name,
|
|
53
|
+
primary: { url: sqlUrl },
|
|
54
|
+
};
|
|
55
|
+
} else if (isKvDecl(decl)) {
|
|
56
|
+
kvBindings[decl.name] = kvUrl !== undefined ? { url: kvUrl } : {};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
34
60
|
const store = createStoreRuntime({
|
|
35
61
|
drivers: {
|
|
36
|
-
sql:
|
|
37
|
-
kv:
|
|
62
|
+
sql: sqlDriverFor(sqlId),
|
|
63
|
+
kv: kvDriverFor(kvId),
|
|
38
64
|
files: memoryDrivers.files,
|
|
39
65
|
index: memoryDrivers.index,
|
|
40
66
|
},
|
|
67
|
+
sql: sqlBindings,
|
|
68
|
+
kv: kvBindings,
|
|
41
69
|
now,
|
|
42
70
|
});
|
|
43
71
|
for (const decl of options.stores ?? []) {
|
|
@@ -45,3 +73,105 @@ export function bindStore(
|
|
|
45
73
|
}
|
|
46
74
|
return store;
|
|
47
75
|
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param options - Boot options
|
|
79
|
+
* @param env - Active env
|
|
80
|
+
* @param stack - Stack mode
|
|
81
|
+
*/
|
|
82
|
+
export function resolveSqlDriverId(
|
|
83
|
+
options: BootOptions,
|
|
84
|
+
env: ConfigEnv,
|
|
85
|
+
stack: boolean,
|
|
86
|
+
): string {
|
|
87
|
+
const fromEnv = process.env.OKE_SQL_DRIVER?.trim();
|
|
88
|
+
if (stack && fromEnv) return fromEnv;
|
|
89
|
+
const resolved = resolveDriverId(options.config?.drivers?.store?.sql, env);
|
|
90
|
+
if (resolved) return resolved;
|
|
91
|
+
return stack ? "postgres" : "memory";
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @param options - Boot options
|
|
96
|
+
* @param env - Active env
|
|
97
|
+
* @param stack - Stack mode
|
|
98
|
+
*/
|
|
99
|
+
export function resolveKvDriverId(
|
|
100
|
+
options: BootOptions,
|
|
101
|
+
env: ConfigEnv,
|
|
102
|
+
stack: boolean,
|
|
103
|
+
): string {
|
|
104
|
+
const fromEnv = process.env.OKE_KV_DRIVER?.trim();
|
|
105
|
+
if (stack && fromEnv) return fromEnv;
|
|
106
|
+
const resolved = resolveDriverId(options.config?.drivers?.store?.kv, env);
|
|
107
|
+
if (resolved) return resolved;
|
|
108
|
+
return stack ? "redis" : "memory";
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function sqlDriverFor(id: string): SqlDriver {
|
|
112
|
+
switch (id) {
|
|
113
|
+
case "postgres":
|
|
114
|
+
return postgresDriver;
|
|
115
|
+
case "sqlite":
|
|
116
|
+
return sqliteDriver;
|
|
117
|
+
case "memory":
|
|
118
|
+
return memoryDrivers.sql;
|
|
119
|
+
default:
|
|
120
|
+
throw new Error(`oke boot: unknown sql driver "${id}"`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function kvDriverFor(id: string): KvDriver {
|
|
125
|
+
switch (id) {
|
|
126
|
+
case "redis":
|
|
127
|
+
return redisDriver;
|
|
128
|
+
case "memory":
|
|
129
|
+
return memoryDrivers.kv;
|
|
130
|
+
default:
|
|
131
|
+
throw new Error(`oke boot: unknown kv driver "${id}"`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function sqlUrlFor(sqlId: string, stack: boolean): string {
|
|
136
|
+
if (sqlId === "postgres") {
|
|
137
|
+
const url =
|
|
138
|
+
process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL ?? undefined;
|
|
139
|
+
if (!url) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
stack
|
|
142
|
+
? "oke boot: postgres driver needs DATABASE_URL (did `oke dev -s` write .env.stack?)"
|
|
143
|
+
: "oke boot: postgres driver needs DATABASE_URL",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return url;
|
|
147
|
+
}
|
|
148
|
+
if (sqlId === "sqlite") {
|
|
149
|
+
return process.env.OKE_SQLITE_URL ?? ".oke/app.sqlite";
|
|
150
|
+
}
|
|
151
|
+
return ":memory:";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function kvUrlFor(kvId: string, stack: boolean): string | undefined {
|
|
155
|
+
if (kvId !== "redis") return undefined;
|
|
156
|
+
const url = process.env.REDIS_URL ?? process.env.OKE_STORE_KV_URL ?? undefined;
|
|
157
|
+
if (!url) {
|
|
158
|
+
throw new Error(
|
|
159
|
+
stack
|
|
160
|
+
? "oke boot: redis driver needs REDIS_URL (did `oke dev -s` write .env.stack?)"
|
|
161
|
+
: "oke boot: redis driver needs REDIS_URL",
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return url;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isSqlDecl(
|
|
168
|
+
decl: StoreDecl,
|
|
169
|
+
): decl is Extract<StoreDecl, { facet: "sql" }> {
|
|
170
|
+
return decl.facet === "sql";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isKvDecl(
|
|
174
|
+
decl: StoreDecl,
|
|
175
|
+
): decl is Extract<StoreDecl, { facet: "kv" }> {
|
|
176
|
+
return decl.facet === "kv";
|
|
177
|
+
}
|