okengine 0.2.3 → 0.2.5
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
package/README.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="site/public/OKE-W.svg" />
|
|
4
|
+
<img alt="OKE" src="site/public/OKE-B.svg" width="220" />
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
7
|
+
|
|
1
8
|
# okengine
|
|
2
9
|
|
|
3
10
|
*"Encore's batteries and dashboard, Elysia's speed and DX, Hono's portability — without the Rust lock-in, the cloud gravity, or the source-available license."*
|
|
@@ -225,7 +232,7 @@ oke images list # recipe · image · tag · digest · size (wri
|
|
|
225
232
|
3. **Derive Docker / compose**, then pin digests so tags cannot drift:
|
|
226
233
|
|
|
227
234
|
```bash
|
|
228
|
-
oke docker # Dockerfile + compose
|
|
235
|
+
oke docker # writes under docker/ (Dockerfile + compose.<role>.yml · …)
|
|
229
236
|
oke docker --prod # -p healthchecks, volumes, limits, secret refs, deploy.replicas
|
|
230
237
|
oke images pin # tags → digests in oke.images.lock
|
|
231
238
|
```
|
|
@@ -251,8 +258,8 @@ bun add okengine # ONE package
|
|
|
251
258
|
|
|
252
259
|
oke dev # watch · hot reload · Console :6533 · app :6530 · MCP :6535
|
|
253
260
|
# → also auto-syncs client types on every save
|
|
254
|
-
oke dev --stack # -s
|
|
255
|
-
oke dev -s store.sql,
|
|
261
|
+
oke dev --stack # -s infra only under docker/ (Postgres/Redis/…); app stays on host Bun
|
|
262
|
+
oke dev -s store.sql,store.kv # partial: only these roles get real backends
|
|
256
263
|
|
|
257
264
|
oke start # runs exactly what production runs (this is the Docker CMD)
|
|
258
265
|
oke doctor # verify secrets, ports, drivers, tenancy, schema drift
|
|
@@ -269,7 +276,7 @@ oke schema generate # core + plugin tables → schema/oke.ts (--c
|
|
|
269
276
|
oke vault set STRIPE_KEY # also: list · import .env · key rotate
|
|
270
277
|
oke client add <url> # types for a separate frontend repo
|
|
271
278
|
|
|
272
|
-
oke docker # Dockerfile + compose
|
|
279
|
+
oke docker # artefacts under docker/ (Dockerfile + compose.<role>.yml · …)
|
|
273
280
|
oke docker --prod # -p healthchecks, volumes, limits, secret refs, deploy.replicas
|
|
274
281
|
oke images list # recipe · image · tag · digest · size (--json|-j)
|
|
275
282
|
oke images pin # tags → digests in oke.images.lock
|
|
@@ -346,6 +353,25 @@ Measured size and latency caps — see [`BUDGETS.md`](BUDGETS.md). Refresh with
|
|
|
346
353
|
|
|
347
354
|
---
|
|
348
355
|
|
|
356
|
+
## Local framework link (try changes without publishing)
|
|
357
|
+
|
|
358
|
+
To exercise a local `okengine` checkout inside an app (e.g. `oke-1`) without npm publish:
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
# in the okengine repo
|
|
362
|
+
bun link
|
|
363
|
+
# if you changed the Console SPA:
|
|
364
|
+
bun run build
|
|
365
|
+
|
|
366
|
+
# in the app repo
|
|
367
|
+
bun link okengine
|
|
368
|
+
# edit okengine → restart `bun dev` in the app
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Alternatively set `"okengine": "file:../okengine"` in the app `package.json` and run `bun install`.
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
349
375
|
## Status
|
|
350
376
|
|
|
351
377
|
Pre-1.0. Published on [npm](https://www.npmjs.com/package/okengine) and [JSR](https://jsr.io/@omqkhafi/okengine) (`okengine` + `create-oke`, lockstep). Docs at [okengine.vercel.app](https://okengine.vercel.app). MIT. No `CONTRIBUTING` yet — issues and PRs welcome.
|
package/docs/spec/example.md
CHANGED
|
@@ -43,7 +43,16 @@ import { defineConfig } from "okengine/config";
|
|
|
43
43
|
|
|
44
44
|
export default defineConfig({
|
|
45
45
|
drivers: {
|
|
46
|
-
|
|
46
|
+
// `stack` = `oke dev -s` (local server). Omitting it copies `prod`
|
|
47
|
+
// (defineConfig); vault `sops` becomes stack `dotenv` for `.env.stack`.
|
|
48
|
+
store: {
|
|
49
|
+
sql: {
|
|
50
|
+
dev: "sqlite",
|
|
51
|
+
stack: "postgres",
|
|
52
|
+
test: "memory",
|
|
53
|
+
prod: "postgres",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
47
56
|
},
|
|
48
57
|
});
|
|
49
58
|
```
|
|
@@ -1132,7 +1141,7 @@ bun add okengine # ONE package
|
|
|
1132
1141
|
|
|
1133
1142
|
oke dev # watch · hot reload · Console :6533 · app :6530 · MCP :6535
|
|
1134
1143
|
# → also auto-syncs client types on every save
|
|
1135
|
-
oke dev --stack # -s
|
|
1144
|
+
oke dev --stack # -s infra compose under docker/ (no app container; host Bun)
|
|
1136
1145
|
oke dev -s store.sql,signal # partial: only these roles get real backends
|
|
1137
1146
|
|
|
1138
1147
|
oke start # runs exactly what production runs (this is the Docker CMD)
|
|
@@ -1143,7 +1152,7 @@ oke schema generate # core + plugin tables → schema/oke.ts (--c
|
|
|
1143
1152
|
oke vault set STRIPE_KEY # also: list · import .env · key rotate
|
|
1144
1153
|
oke client add <url> # types for a separate frontend repo
|
|
1145
1154
|
|
|
1146
|
-
oke docker # Dockerfile + compose
|
|
1155
|
+
oke docker # docker/Dockerfile + docker/compose.<role>.yml · …
|
|
1147
1156
|
oke docker --prod # healthchecks, volumes, limits, secret refs, deploy.replicas
|
|
1148
1157
|
oke images pin # tags → digests in oke.images.lock
|
|
1149
1158
|
|
|
@@ -43,7 +43,14 @@ import { defineConfig } from "okengine/config";
|
|
|
43
43
|
|
|
44
44
|
export default defineConfig({
|
|
45
45
|
drivers: {
|
|
46
|
-
store: {
|
|
46
|
+
store: {
|
|
47
|
+
sql: {
|
|
48
|
+
dev: "sqlite",
|
|
49
|
+
stack: "postgres",
|
|
50
|
+
test: "memory",
|
|
51
|
+
prod: "postgres",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
47
54
|
},
|
|
48
55
|
});
|
|
49
56
|
```
|
|
@@ -1132,7 +1139,7 @@ bun add okengine # ONE package
|
|
|
1132
1139
|
|
|
1133
1140
|
oke dev # watch · hot reload · Console :6533 · app :6530 · MCP :6535
|
|
1134
1141
|
# → also auto-syncs client types on every save
|
|
1135
|
-
oke dev --stack # -s
|
|
1142
|
+
oke dev --stack # -s infra compose under docker/ (no app container; host Bun)
|
|
1136
1143
|
oke dev -s store.sql,signal # partial: only these roles get real backends
|
|
1137
1144
|
|
|
1138
1145
|
oke start # runs exactly what production runs (this is the Docker CMD)
|
|
@@ -1143,7 +1150,7 @@ oke schema generate # core + plugin tables → schema/oke.ts (--c
|
|
|
1143
1150
|
oke vault set STRIPE_KEY # also: list · import .env · key rotate
|
|
1144
1151
|
oke client add <url> # types for a separate frontend repo
|
|
1145
1152
|
|
|
1146
|
-
oke docker # Dockerfile + compose
|
|
1153
|
+
oke docker # docker/Dockerfile + docker/compose.<role>.yml · …
|
|
1147
1154
|
oke docker --prod # healthchecks, volumes, limits, secret refs, deploy.replicas
|
|
1148
1155
|
oke images pin # tags → digests in oke.images.lock
|
|
1149
1156
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "okengine",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "One law. Eight elements. Ten exports. One package. One manifest. Every backend need is derived, never added.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -17,51 +17,20 @@
|
|
|
17
17
|
"scripts": {
|
|
18
18
|
"typecheck": "tsc --noEmit",
|
|
19
19
|
"test": "bun test --pass-with-no-tests",
|
|
20
|
-
"test:
|
|
21
|
-
"test:runtime": "bun test src/runtime",
|
|
22
|
-
"test:compiler": "bun test src/validation src/compiler/aot src/compiler/differential src/compiler/extract",
|
|
23
|
-
"test:client": "bun test src/client src/cli/client-add.test.ts",
|
|
24
|
-
"test:store": "bun test src/elements/store src/drivers",
|
|
25
|
-
"test:signal": "bun test src/elements/signal",
|
|
26
|
-
"test:clock": "bun test src/elements/clock",
|
|
27
|
-
"test:gate": "bun test src/elements/gate",
|
|
28
|
-
"test:vault": "bun test src/elements/vault",
|
|
29
|
-
"test:channel": "bun test src/elements/channel",
|
|
30
|
-
"test:ai": "bun test src/elements/ai",
|
|
31
|
-
"test:auth": "bun test src/auth",
|
|
32
|
-
"test:runs": "bun test src/runs",
|
|
33
|
-
"test:cli": "bun test src/cli src/docker",
|
|
34
|
-
"test:console": "bun test src/console/server && bun test src/console/ui/flows && bun test src/console/ui/traces && bun test src/console/ui/runs && bun test src/console/ui/signals && bun test src/console/ui/architecture && bun test src/console/xss.gate.test.ts src/console/budget.test.ts",
|
|
35
|
-
"test:mcp": "bun test src/mcp",
|
|
20
|
+
"test:coverage": "bun test --coverage --pass-with-no-tests",
|
|
36
21
|
"test:console:smoke": "bunx playwright test tests/console/smoke.spec.ts",
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"bench
|
|
41
|
-
"bench:console": "bun test src/console/budget.test.ts",
|
|
42
|
-
"bench:kernel": "bun test src/kernel/budget.test.ts",
|
|
43
|
-
"bench:routing": "bun test src/kernel/routing-budget.test.ts",
|
|
44
|
-
"bench:runs": "bun test src/runs/bench.test.ts",
|
|
22
|
+
"test:integration": "bun test examples/notes examples/linkly examples/provisions examples/skyport examples/skyport/tests/docker.test.ts && bun run --cwd packages/create-oke typecheck && bun test packages/create-oke/src && CREATE_OKE_INTEGRATION=1 bun test --timeout 180000 packages/create-oke/tests/scaffold.integration.test.ts packages/create-oke/tests/dev.integration.test.ts",
|
|
23
|
+
"build": "bunx vite build --config src/console/ui/vite.config.ts",
|
|
24
|
+
"prepack": "bun run build",
|
|
25
|
+
"bench": "bun test ./src/runtime/cold-start.bench.ts src/client/budget.test.ts src/console/budget.test.ts src/kernel/budget.test.ts src/kernel/routing-budget.test.ts src/runs/bench.test.ts",
|
|
45
26
|
"budgets": "bun src/release/publish.ts",
|
|
46
|
-
"gate
|
|
47
|
-
"gate:errors": "bun test src/kernel/errors.registry.test.ts",
|
|
48
|
-
"gate:codemods": "bun test src/upgrade/codemods.test.ts",
|
|
49
|
-
"test:examples": "bun test examples/notes && bun test examples/linkly && bun test examples/provisions && bun test examples/skyport",
|
|
50
|
-
"test:create-oke": "bun run --cwd packages/create-oke typecheck && bun test packages/create-oke/src && CREATE_OKE_INTEGRATION=1 bun test --timeout 180000 packages/create-oke/tests/scaffold.integration.test.ts packages/create-oke/tests/dev.integration.test.ts",
|
|
51
|
-
"test:doc-drift": "bun src/cli/doc-drift.ts",
|
|
27
|
+
"gate": "bun src/cli/doctor-diff-examples.ts && bun test src/kernel/errors.registry.test.ts src/upgrade/codemods.test.ts scripts/publish.gate.test.ts && bun src/cli/doc-drift.ts",
|
|
52
28
|
"site:sync": "bun run --cwd site sync",
|
|
53
29
|
"site:dev": "bun run --cwd site dev",
|
|
54
30
|
"site:build": "bun run --cwd site build",
|
|
55
|
-
"
|
|
56
|
-
"ci": "bun run bench:cold-start && bun run typecheck && bun run test && bun run budgets && bun run gate:doctor-diff && bun run gate:errors && bun run gate:codemods && bun run test:doc-drift && bun run site:build && bun run test:examples && bun run test:skyport-docker && bun run test:create-oke",
|
|
57
|
-
"gate:publish": "bun test scripts/publish.gate.test.ts",
|
|
31
|
+
"ci": "bun run bench && bun run typecheck && bun run test && bun run budgets && bun run gate && bun run site:build && bun run test:integration",
|
|
58
32
|
"bump": "bun run scripts/bump-version.ts",
|
|
59
|
-
"publish
|
|
60
|
-
"publish:npm": "bun run scripts/publish.ts -- --npm-only",
|
|
61
|
-
"publish:jsr": "bun run scripts/publish.ts -- --jsr-only",
|
|
62
|
-
"publish:all:dry": "bun run scripts/publish.ts -- --dry-run",
|
|
63
|
-
"publish:npm:dry": "bun run scripts/publish.ts -- --dry-run --npm-only",
|
|
64
|
-
"publish:jsr:dry": "bun run scripts/publish.ts -- --dry-run --jsr-only",
|
|
33
|
+
"publish": "bun run scripts/publish.ts",
|
|
65
34
|
"g": "gflows"
|
|
66
35
|
},
|
|
67
36
|
"dependencies": {
|
|
@@ -11,17 +11,29 @@
|
|
|
11
11
|
* - `PORT` — listen port (`0` = ephemeral)
|
|
12
12
|
* - `OKE_HOSTNAME` — listen hostname (default `127.0.0.1`)
|
|
13
13
|
* - `OKE_READY_PATH` — when set, write bound port here once listening
|
|
14
|
+
* - `OKE_DEV_HERO_CONSOLE` / `OKE_DEV_HERO_MCP` — URL lines for soft-reload hero
|
|
15
|
+
* - `OKE_DEV_HERO_META` — JSON {@link import("./hero-meta.ts").DevHeroSnapshot}
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
18
|
import { resolve } from "node:path";
|
|
17
19
|
import { pathToFileURL } from "node:url";
|
|
18
20
|
import { createBunRuntime } from "../runtime/bun.ts";
|
|
19
21
|
import { APP_PORT, type FetchApp } from "../runtime/types.ts";
|
|
20
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
clearTerminalScreen,
|
|
24
|
+
formatAppReadyLine,
|
|
25
|
+
formatDevHero,
|
|
26
|
+
} from "../term.ts";
|
|
27
|
+
import { decodeHeroSnapshot } from "./hero-meta.ts";
|
|
21
28
|
|
|
22
29
|
/** Stable Bun.serve id so `--hot` reuses the socket across soft reloads. */
|
|
23
30
|
export const DEV_APP_SERVE_ID = "oke-dev-app";
|
|
24
31
|
|
|
32
|
+
/** Survives `bun --hot` soft reload — marks second+ evaluation. */
|
|
33
|
+
type DevRunnerGlobals = typeof globalThis & {
|
|
34
|
+
__okeDevSoftReload?: boolean;
|
|
35
|
+
};
|
|
36
|
+
|
|
25
37
|
/** App entry shape — FetchApp plus boot before serve. */
|
|
26
38
|
type BootableApp = FetchApp & {
|
|
27
39
|
boot(): Promise<unknown>;
|
|
@@ -36,6 +48,9 @@ if (entry === undefined || entry.length === 0) {
|
|
|
36
48
|
const port = Number(Bun.env["PORT"] ?? APP_PORT);
|
|
37
49
|
const hostname = Bun.env["OKE_HOSTNAME"] ?? "127.0.0.1";
|
|
38
50
|
const readyPath = Bun.env["OKE_READY_PATH"];
|
|
51
|
+
const consoleUrl = Bun.env["OKE_DEV_HERO_CONSOLE"];
|
|
52
|
+
const mcpUrl = Bun.env["OKE_DEV_HERO_MCP"];
|
|
53
|
+
const heroMeta = decodeHeroSnapshot(Bun.env["OKE_DEV_HERO_META"]);
|
|
39
54
|
|
|
40
55
|
const absoluteEntry = resolve(entry);
|
|
41
56
|
const mod = (await import(pathToFileURL(absoluteEntry).href)) as {
|
|
@@ -60,11 +75,35 @@ const handle = createBunRuntime().serve(mod.app, {
|
|
|
60
75
|
id: DEV_APP_SERVE_ID,
|
|
61
76
|
});
|
|
62
77
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
78
|
+
const appUrl = `http://${formatHostForUrl(hostname)}:${handle.port}`;
|
|
79
|
+
const g = globalThis as DevRunnerGlobals;
|
|
80
|
+
const isSoftReload = g.__okeDevSoftReload === true;
|
|
81
|
+
g.__okeDevSoftReload = true;
|
|
82
|
+
|
|
83
|
+
if (
|
|
84
|
+
isSoftReload &&
|
|
85
|
+
consoleUrl !== undefined &&
|
|
86
|
+
consoleUrl.length > 0 &&
|
|
87
|
+
mcpUrl !== undefined &&
|
|
88
|
+
mcpUrl.length > 0
|
|
89
|
+
) {
|
|
90
|
+
// Drop request logs; keep the hero (Bun --no-clear-screen + our reprint).
|
|
91
|
+
process.stdout.write(clearTerminalScreen());
|
|
92
|
+
process.stdout.write(
|
|
93
|
+
formatDevHero({
|
|
94
|
+
appUrl,
|
|
95
|
+
consoleUrl,
|
|
96
|
+
mcpUrl,
|
|
97
|
+
profile: heroMeta?.profile,
|
|
98
|
+
runtimeEnv: heroMeta?.runtimeEnv,
|
|
99
|
+
system: heroMeta?.system,
|
|
100
|
+
elements: heroMeta?.elements,
|
|
101
|
+
version: heroMeta?.version,
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
} else {
|
|
105
|
+
process.stdout.write(formatAppReadyLine(appUrl));
|
|
106
|
+
}
|
|
68
107
|
|
|
69
108
|
if (readyPath !== undefined && readyPath.length > 0) {
|
|
70
109
|
await Bun.write(readyPath, `${handle.port}\n`);
|
package/src/cli/dev.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `oke dev` — watch · hot reload · Console :6533 · MCP :6535 · client types on save.
|
|
3
|
-
* `oke dev --stack`
|
|
3
|
+
* `oke dev --stack` boots infra compose and binds prod store drivers (Postgres/Redis).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { watch } from "node:fs";
|
|
@@ -11,8 +11,12 @@ import type { SessionStore } from "../auth/sessions.ts";
|
|
|
11
11
|
import type { ConsoleAppHandle } from "../console/server/app.ts";
|
|
12
12
|
import type { ConsoleState } from "../console/server/state.ts";
|
|
13
13
|
import {
|
|
14
|
+
DEFAULT_DOCKER_DIR,
|
|
14
15
|
deriveInfrastructure,
|
|
15
16
|
formatStackEnv,
|
|
17
|
+
loadExistingStackCredentials,
|
|
18
|
+
stackAppSlug,
|
|
19
|
+
stackInstanceId,
|
|
16
20
|
writeDerivedFiles,
|
|
17
21
|
type DeriveOptions,
|
|
18
22
|
} from "../docker/index.ts";
|
|
@@ -25,11 +29,22 @@ import {
|
|
|
25
29
|
} from "../runtime/types.ts";
|
|
26
30
|
import {
|
|
27
31
|
formatDevBanner,
|
|
32
|
+
formatDevLogSeparator,
|
|
28
33
|
formatServiceLine,
|
|
34
|
+
formatStackSummary,
|
|
29
35
|
formatStatusLine,
|
|
30
36
|
} from "../term.ts";
|
|
31
37
|
import { clientAdd } from "./client-add.ts";
|
|
32
|
-
import {
|
|
38
|
+
import { resolveDriverId } from "../config/index.ts";
|
|
39
|
+
import {
|
|
40
|
+
buildDevHeroSnapshot,
|
|
41
|
+
encodeHeroSnapshot,
|
|
42
|
+
} from "./hero-meta.ts";
|
|
43
|
+
import {
|
|
44
|
+
loadManifest,
|
|
45
|
+
loadOkeConfig,
|
|
46
|
+
resolveImages,
|
|
47
|
+
} from "./load-config.ts";
|
|
33
48
|
import { mcpContextFromConsole } from "./mcp-from-console.ts";
|
|
34
49
|
import { resolveDevPorts } from "./ports.ts";
|
|
35
50
|
|
|
@@ -224,16 +239,36 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
224
239
|
let stackRoles: string[] | null = null;
|
|
225
240
|
let composeFiles: string[] | null = null;
|
|
226
241
|
let stackEnv: Record<string, string> | null = null;
|
|
242
|
+
let stackSqlDriver = "postgres";
|
|
243
|
+
let stackKvDriver = "redis";
|
|
244
|
+
let loadedConfig: Awaited<ReturnType<typeof loadOkeConfig>>["config"] | null =
|
|
245
|
+
null;
|
|
246
|
+
try {
|
|
247
|
+
loadedConfig = (await loadOkeConfig(cwd)).config;
|
|
248
|
+
} catch {
|
|
249
|
+
loadedConfig = null;
|
|
250
|
+
}
|
|
227
251
|
|
|
228
252
|
if (options.stack) {
|
|
229
253
|
let images = options.images;
|
|
230
254
|
if (!images) {
|
|
255
|
+
if (!loadedConfig) {
|
|
256
|
+
try {
|
|
257
|
+
const loaded = await loadOkeConfig(cwd);
|
|
258
|
+
loadedConfig = loaded.config;
|
|
259
|
+
images = resolveImages(loaded.config);
|
|
260
|
+
} catch (err) {
|
|
261
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
262
|
+
return { code: 1 };
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
images = resolveImages(loadedConfig);
|
|
266
|
+
}
|
|
267
|
+
} else if (!loadedConfig) {
|
|
231
268
|
try {
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
236
|
-
return { code: 1 };
|
|
269
|
+
loadedConfig = (await loadOkeConfig(cwd)).config;
|
|
270
|
+
} catch {
|
|
271
|
+
loadedConfig = null;
|
|
237
272
|
}
|
|
238
273
|
}
|
|
239
274
|
if (Array.isArray(options.stack)) {
|
|
@@ -246,16 +281,31 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
246
281
|
stackRoles = Object.keys(images);
|
|
247
282
|
}
|
|
248
283
|
|
|
284
|
+
stackSqlDriver =
|
|
285
|
+
resolveDriverId(loadedConfig?.drivers?.store?.sql, "prod") ?? "postgres";
|
|
286
|
+
stackKvDriver =
|
|
287
|
+
resolveDriverId(loadedConfig?.drivers?.store?.kv, "prod") ?? "redis";
|
|
288
|
+
|
|
289
|
+
const composeDir = DEFAULT_DOCKER_DIR;
|
|
290
|
+
const dockerOut = resolve(cwd, composeDir);
|
|
291
|
+
const instanceId = stackInstanceId(cwd);
|
|
292
|
+
const appSlug = stackAppSlug(cwd);
|
|
293
|
+
const reusedCreds =
|
|
294
|
+
options.credentials ??
|
|
295
|
+
(await loadExistingStackCredentials(cwd, stackRoles));
|
|
249
296
|
const derived = deriveInfrastructure({
|
|
250
297
|
images,
|
|
251
|
-
app:
|
|
298
|
+
app: appSlug,
|
|
252
299
|
prod: false,
|
|
253
|
-
|
|
300
|
+
includeApp: false,
|
|
301
|
+
composeDir,
|
|
302
|
+
instanceId,
|
|
303
|
+
...(reusedCreds ? { credentials: reusedCreds } : {}),
|
|
254
304
|
host: "127.0.0.1",
|
|
255
305
|
});
|
|
256
306
|
// Layer 4 is user-owned — include only if the file already exists.
|
|
257
307
|
const existingOverride = await Bun.file(
|
|
258
|
-
resolve(
|
|
308
|
+
resolve(dockerOut, "compose.override.yml"),
|
|
259
309
|
).exists();
|
|
260
310
|
composeFiles = derived.composeFiles.filter(
|
|
261
311
|
(f) => f !== "compose.override.yml" || existingOverride,
|
|
@@ -263,8 +313,11 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
263
313
|
stackEnv = { ...derived.stackEnv };
|
|
264
314
|
|
|
265
315
|
if (!options.dryRun) {
|
|
266
|
-
await writeDerivedFiles(derived,
|
|
267
|
-
|
|
316
|
+
await writeDerivedFiles(derived, dockerOut, {
|
|
317
|
+
writeStackEnv: true,
|
|
318
|
+
stackEnvDir: cwd,
|
|
319
|
+
});
|
|
320
|
+
// Ensure .env.stack is at project root for the host Bun app / vault.
|
|
268
321
|
await Bun.write(resolve(cwd, ".env.stack"), formatStackEnv(stackEnv));
|
|
269
322
|
const up =
|
|
270
323
|
options.composeUp ??
|
|
@@ -281,8 +334,27 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
281
334
|
throw new Error(`oke dev --stack: docker compose exited ${code}`);
|
|
282
335
|
}
|
|
283
336
|
});
|
|
284
|
-
|
|
285
|
-
|
|
337
|
+
// Compose files live under docker/; cwd for compose is that directory.
|
|
338
|
+
await up(composeFiles, dockerOut);
|
|
339
|
+
const roleLabel = (role: string): string => {
|
|
340
|
+
if (role === "store.sql") return "postgres";
|
|
341
|
+
if (role === "store.kv") return "redis";
|
|
342
|
+
return role;
|
|
343
|
+
};
|
|
344
|
+
const appDrivers = [
|
|
345
|
+
...(stackRoles.includes("store.sql") ? [stackSqlDriver] : []),
|
|
346
|
+
...(stackRoles.includes("store.kv") ? [stackKvDriver] : []),
|
|
347
|
+
];
|
|
348
|
+
write(
|
|
349
|
+
formatStackSummary({
|
|
350
|
+
project: `oke-${appSlug}`,
|
|
351
|
+
services: derived.specs.map((spec) => ({
|
|
352
|
+
label: roleLabel(spec.role),
|
|
353
|
+
hostPort: spec.hostPort,
|
|
354
|
+
})),
|
|
355
|
+
appDrivers,
|
|
356
|
+
}),
|
|
357
|
+
);
|
|
286
358
|
}
|
|
287
359
|
}
|
|
288
360
|
|
|
@@ -296,7 +368,34 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
296
368
|
stackEnv,
|
|
297
369
|
};
|
|
298
370
|
|
|
299
|
-
|
|
371
|
+
let okeVersion = "0.0.0";
|
|
372
|
+
try {
|
|
373
|
+
const pkg = (await Bun.file(
|
|
374
|
+
resolve(import.meta.dir, "../../package.json"),
|
|
375
|
+
).json()) as { version?: string };
|
|
376
|
+
if (typeof pkg.version === "string") okeVersion = pkg.version;
|
|
377
|
+
} catch {
|
|
378
|
+
// shipped binary may not sit next to package.json
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const heroSnapshot = buildDevHeroSnapshot({
|
|
382
|
+
config: loadedConfig,
|
|
383
|
+
stack: Boolean(options.stack),
|
|
384
|
+
sqlDriver: options.stack ? stackSqlDriver : undefined,
|
|
385
|
+
kvDriver: options.stack ? stackKvDriver : undefined,
|
|
386
|
+
version: okeVersion,
|
|
387
|
+
nodeEnv: "development",
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
write(
|
|
391
|
+
formatDevBanner({
|
|
392
|
+
profile: heroSnapshot.profile,
|
|
393
|
+
runtimeEnv: heroSnapshot.runtimeEnv,
|
|
394
|
+
system: heroSnapshot.system,
|
|
395
|
+
elements: heroSnapshot.elements,
|
|
396
|
+
version: okeVersion,
|
|
397
|
+
}),
|
|
398
|
+
);
|
|
300
399
|
|
|
301
400
|
if (options.dryRun) return { code: 0, plan };
|
|
302
401
|
|
|
@@ -304,8 +403,21 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
304
403
|
...(process.env as Record<string, string>),
|
|
305
404
|
NODE_ENV: "development",
|
|
306
405
|
PORT: String(appPort),
|
|
406
|
+
OKE_DEV_REQUEST_LOG: "1",
|
|
407
|
+
/** Soft-reload hero reprint (Console / MCP URLs + element snapshot). */
|
|
408
|
+
OKE_DEV_HERO_CONSOLE: `http://127.0.0.1:${consolePort}`,
|
|
409
|
+
OKE_DEV_HERO_MCP: `http://127.0.0.1:${mcpPort}`,
|
|
410
|
+
OKE_DEV_HERO_META: encodeHeroSnapshot(heroSnapshot),
|
|
307
411
|
...(stackEnv ?? {}),
|
|
412
|
+
...(stackEnv
|
|
413
|
+
? {
|
|
414
|
+
OKE_STACK: "1",
|
|
415
|
+
OKE_SQL_DRIVER: stackSqlDriver,
|
|
416
|
+
OKE_KV_DRIVER: stackKvDriver,
|
|
417
|
+
}
|
|
418
|
+
: {}),
|
|
308
419
|
};
|
|
420
|
+
process.env.OKE_DEV_REQUEST_LOG = "1";
|
|
309
421
|
|
|
310
422
|
const startApp =
|
|
311
423
|
options.startApp ??
|
|
@@ -429,6 +541,7 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
429
541
|
const appUrl =
|
|
430
542
|
app.url?.origin ?? `http://127.0.0.1:${boundAppPort}`;
|
|
431
543
|
await regen(appUrl);
|
|
544
|
+
write(formatDevLogSeparator());
|
|
432
545
|
|
|
433
546
|
const watcher = watch(
|
|
434
547
|
resolve(cwd, "src"),
|
|
@@ -555,9 +668,13 @@ async function startAppHot(
|
|
|
555
668
|
OKE_ENTRY: absoluteEntry,
|
|
556
669
|
OKE_HOSTNAME: hostname,
|
|
557
670
|
OKE_READY_PATH: readyPath,
|
|
671
|
+
OKE_DEV_REQUEST_LOG: "1",
|
|
672
|
+
OKE_DEV_SURFACE: "App",
|
|
558
673
|
};
|
|
559
674
|
|
|
560
|
-
|
|
675
|
+
// `--no-clear-screen`: Bun must not wipe the hero; the runner clears only
|
|
676
|
+
// request logs on soft reload and reprints App / Console / MCP URLs.
|
|
677
|
+
const proc = Bun.spawn(["bun", "--hot", "--no-clear-screen", runner], {
|
|
561
678
|
cwd,
|
|
562
679
|
env,
|
|
563
680
|
stdout: "inherit",
|
|
@@ -652,7 +769,9 @@ export async function devCli(args: readonly string[]): Promise<number> {
|
|
|
652
769
|
|
|
653
770
|
Watch · hot reload · Console :6533 · app :6530 · MCP :6535
|
|
654
771
|
Regenerates client types on every save.
|
|
655
|
-
--stack boots
|
|
772
|
+
--stack boots infra under docker/ and runs the app on host Bun with prod
|
|
773
|
+
store drivers (Postgres/Redis) so local mimics the server.
|
|
774
|
+
Partial roles: -s store.sql,store.kv.
|
|
656
775
|
`);
|
|
657
776
|
return 0;
|
|
658
777
|
}
|
|
@@ -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;
|