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/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.4",
|
|
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,12 +29,24 @@ 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";
|
|
49
|
+
import { resolveDevPorts } from "./ports.ts";
|
|
34
50
|
|
|
35
51
|
/** Max wait for the `bun --hot` app child to bind a port. */
|
|
36
52
|
const APP_READY_TIMEOUT_MS = 30_000;
|
|
@@ -183,9 +199,27 @@ export interface DevResult {
|
|
|
183
199
|
export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
184
200
|
const write = options.write ?? ((t) => process.stdout.write(t));
|
|
185
201
|
const cwd = options.cwd ?? process.cwd();
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
const
|
|
202
|
+
const preferredApp = options.appPort ?? Number(Bun.env.PORT ?? APP_PORT);
|
|
203
|
+
const preferredConsole = options.consolePort ?? CONSOLE_PORT;
|
|
204
|
+
const preferredMcp = options.mcpPort ?? MCP_PORT;
|
|
205
|
+
// Explicit overrides (incl. `0` ephemeral) skip probing; otherwise +1 until free.
|
|
206
|
+
const ports =
|
|
207
|
+
options.appPort !== undefined ||
|
|
208
|
+
options.consolePort !== undefined ||
|
|
209
|
+
options.mcpPort !== undefined
|
|
210
|
+
? {
|
|
211
|
+
app: preferredApp,
|
|
212
|
+
console: preferredConsole,
|
|
213
|
+
mcp: preferredMcp,
|
|
214
|
+
}
|
|
215
|
+
: await resolveDevPorts({
|
|
216
|
+
app: preferredApp,
|
|
217
|
+
console: preferredConsole,
|
|
218
|
+
mcp: preferredMcp,
|
|
219
|
+
});
|
|
220
|
+
const appPort = ports.app;
|
|
221
|
+
const consolePort = ports.console;
|
|
222
|
+
const mcpPort = ports.mcp;
|
|
189
223
|
const keepAlive = options.keepAlive ?? true;
|
|
190
224
|
|
|
191
225
|
let entry = options.entry;
|
|
@@ -205,16 +239,36 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
205
239
|
let stackRoles: string[] | null = null;
|
|
206
240
|
let composeFiles: string[] | null = null;
|
|
207
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
|
+
}
|
|
208
251
|
|
|
209
252
|
if (options.stack) {
|
|
210
253
|
let images = options.images;
|
|
211
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) {
|
|
212
268
|
try {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
console.error(err instanceof Error ? err.message : String(err));
|
|
217
|
-
return { code: 1 };
|
|
269
|
+
loadedConfig = (await loadOkeConfig(cwd)).config;
|
|
270
|
+
} catch {
|
|
271
|
+
loadedConfig = null;
|
|
218
272
|
}
|
|
219
273
|
}
|
|
220
274
|
if (Array.isArray(options.stack)) {
|
|
@@ -227,16 +281,31 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
227
281
|
stackRoles = Object.keys(images);
|
|
228
282
|
}
|
|
229
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));
|
|
230
296
|
const derived = deriveInfrastructure({
|
|
231
297
|
images,
|
|
232
|
-
app:
|
|
298
|
+
app: appSlug,
|
|
233
299
|
prod: false,
|
|
234
|
-
|
|
300
|
+
includeApp: false,
|
|
301
|
+
composeDir,
|
|
302
|
+
instanceId,
|
|
303
|
+
...(reusedCreds ? { credentials: reusedCreds } : {}),
|
|
235
304
|
host: "127.0.0.1",
|
|
236
305
|
});
|
|
237
306
|
// Layer 4 is user-owned — include only if the file already exists.
|
|
238
307
|
const existingOverride = await Bun.file(
|
|
239
|
-
resolve(
|
|
308
|
+
resolve(dockerOut, "compose.override.yml"),
|
|
240
309
|
).exists();
|
|
241
310
|
composeFiles = derived.composeFiles.filter(
|
|
242
311
|
(f) => f !== "compose.override.yml" || existingOverride,
|
|
@@ -244,8 +313,11 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
244
313
|
stackEnv = { ...derived.stackEnv };
|
|
245
314
|
|
|
246
315
|
if (!options.dryRun) {
|
|
247
|
-
await writeDerivedFiles(derived,
|
|
248
|
-
|
|
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.
|
|
249
321
|
await Bun.write(resolve(cwd, ".env.stack"), formatStackEnv(stackEnv));
|
|
250
322
|
const up =
|
|
251
323
|
options.composeUp ??
|
|
@@ -262,8 +334,27 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
262
334
|
throw new Error(`oke dev --stack: docker compose exited ${code}`);
|
|
263
335
|
}
|
|
264
336
|
});
|
|
265
|
-
|
|
266
|
-
|
|
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
|
+
);
|
|
267
358
|
}
|
|
268
359
|
}
|
|
269
360
|
|
|
@@ -277,7 +368,34 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
277
368
|
stackEnv,
|
|
278
369
|
};
|
|
279
370
|
|
|
280
|
-
|
|
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
|
+
);
|
|
281
399
|
|
|
282
400
|
if (options.dryRun) return { code: 0, plan };
|
|
283
401
|
|
|
@@ -285,8 +403,21 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
285
403
|
...(process.env as Record<string, string>),
|
|
286
404
|
NODE_ENV: "development",
|
|
287
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),
|
|
288
411
|
...(stackEnv ?? {}),
|
|
412
|
+
...(stackEnv
|
|
413
|
+
? {
|
|
414
|
+
OKE_STACK: "1",
|
|
415
|
+
OKE_SQL_DRIVER: stackSqlDriver,
|
|
416
|
+
OKE_KV_DRIVER: stackKvDriver,
|
|
417
|
+
}
|
|
418
|
+
: {}),
|
|
289
419
|
};
|
|
420
|
+
process.env.OKE_DEV_REQUEST_LOG = "1";
|
|
290
421
|
|
|
291
422
|
const startApp =
|
|
292
423
|
options.startApp ??
|
|
@@ -297,6 +428,20 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
297
428
|
? options.manifest
|
|
298
429
|
: await tryLoadProjectManifest(cwd);
|
|
299
430
|
|
|
431
|
+
async function refreshManifestInto(
|
|
432
|
+
state: ConsoleState | null,
|
|
433
|
+
): Promise<void> {
|
|
434
|
+
if (!state) return;
|
|
435
|
+
try {
|
|
436
|
+
const { extractManifest } = await import("../compiler/extract.ts");
|
|
437
|
+
const { feedManifest } = await import("../console/server/live.ts");
|
|
438
|
+
const next = await extractManifest({ rootDir: cwd });
|
|
439
|
+
feedManifest(state, next);
|
|
440
|
+
} catch {
|
|
441
|
+
// Source may be mid-edit — keep the last good Manifest.
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
300
445
|
const serveConsole =
|
|
301
446
|
options.serveConsole ??
|
|
302
447
|
(async (port) => {
|
|
@@ -396,12 +541,17 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
396
541
|
const appUrl =
|
|
397
542
|
app.url?.origin ?? `http://127.0.0.1:${boundAppPort}`;
|
|
398
543
|
await regen(appUrl);
|
|
544
|
+
write(formatDevLogSeparator());
|
|
399
545
|
|
|
400
546
|
const watcher = watch(
|
|
401
547
|
resolve(cwd, "src"),
|
|
402
548
|
{ recursive: true },
|
|
403
549
|
() => {
|
|
404
550
|
void regen(appUrl);
|
|
551
|
+
// Only live-extract when the host did not pin a Manifest (tests).
|
|
552
|
+
if (options.manifest === undefined) {
|
|
553
|
+
void refreshManifestInto(consoleState);
|
|
554
|
+
}
|
|
405
555
|
},
|
|
406
556
|
);
|
|
407
557
|
|
|
@@ -453,20 +603,45 @@ export async function runDev(options: DevOptions = {}): Promise<DevResult> {
|
|
|
453
603
|
}
|
|
454
604
|
|
|
455
605
|
/**
|
|
456
|
-
*
|
|
606
|
+
* True when a Manifest has no declared flows (scaffold / empty extract).
|
|
607
|
+
*
|
|
608
|
+
* @param manifest - Candidate
|
|
609
|
+
*/
|
|
610
|
+
function isSparseManifest(manifest: Manifest): boolean {
|
|
611
|
+
const flows = manifest.flows;
|
|
612
|
+
if (!flows || typeof flows !== "object") return true;
|
|
613
|
+
return Object.keys(flows).length === 0;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Load Manifest — prefer AoT extract from `src/` when it has flows;
|
|
618
|
+
* otherwise on-disk JSON snapshots; otherwise a sparse extract.
|
|
457
619
|
*
|
|
458
620
|
* @param cwd - Project root
|
|
459
621
|
*/
|
|
460
622
|
async function tryLoadProjectManifest(
|
|
461
623
|
cwd: string,
|
|
462
624
|
): Promise<Manifest | null | undefined> {
|
|
625
|
+
let extracted: Manifest | undefined;
|
|
626
|
+
try {
|
|
627
|
+
const { extractManifest } = await import("../compiler/extract.ts");
|
|
628
|
+
extracted = await extractManifest({ rootDir: cwd });
|
|
629
|
+
} catch {
|
|
630
|
+
// Fall through to JSON snapshots.
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
let fromDisk: Manifest | undefined;
|
|
463
634
|
for (const name of ["oke.manifest.json", "manifest.oke.json"]) {
|
|
464
635
|
const path = resolve(cwd, name);
|
|
465
636
|
if (await Bun.file(path).exists()) {
|
|
466
|
-
|
|
637
|
+
fromDisk = await loadManifest(path);
|
|
638
|
+
break;
|
|
467
639
|
}
|
|
468
640
|
}
|
|
469
|
-
|
|
641
|
+
|
|
642
|
+
if (extracted && !isSparseManifest(extracted)) return extracted;
|
|
643
|
+
if (fromDisk) return fromDisk;
|
|
644
|
+
return extracted;
|
|
470
645
|
}
|
|
471
646
|
|
|
472
647
|
/**
|
|
@@ -493,9 +668,13 @@ async function startAppHot(
|
|
|
493
668
|
OKE_ENTRY: absoluteEntry,
|
|
494
669
|
OKE_HOSTNAME: hostname,
|
|
495
670
|
OKE_READY_PATH: readyPath,
|
|
671
|
+
OKE_DEV_REQUEST_LOG: "1",
|
|
672
|
+
OKE_DEV_SURFACE: "App",
|
|
496
673
|
};
|
|
497
674
|
|
|
498
|
-
|
|
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], {
|
|
499
678
|
cwd,
|
|
500
679
|
env,
|
|
501
680
|
stdout: "inherit",
|
|
@@ -590,7 +769,9 @@ export async function devCli(args: readonly string[]): Promise<number> {
|
|
|
590
769
|
|
|
591
770
|
Watch · hot reload · Console :6533 · app :6530 · MCP :6535
|
|
592
771
|
Regenerates client types on every save.
|
|
593
|
-
--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.
|
|
594
775
|
`);
|
|
595
776
|
return 0;
|
|
596
777
|
}
|