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.
Files changed (66) hide show
  1. package/README.md +30 -4
  2. package/docs/spec/example.md +12 -3
  3. package/docs/spec/four-applications.md +10 -3
  4. package/package.json +9 -40
  5. package/src/cli/dev-app-runner.ts +45 -6
  6. package/src/cli/dev.ts +204 -23
  7. package/src/cli/docker-cli.test.ts +10 -8
  8. package/src/cli/docker.ts +18 -7
  9. package/src/cli/doctor.ts +3 -18
  10. package/src/cli/hero-meta.test.ts +85 -0
  11. package/src/cli/hero-meta.ts +252 -0
  12. package/src/cli/index.ts +4 -1
  13. package/src/cli/json-out.test.ts +1 -1
  14. package/src/cli/load-config.images.test.ts +126 -0
  15. package/src/cli/load-config.ts +145 -2
  16. package/src/cli/ports.test.ts +51 -0
  17. package/src/cli/ports.ts +81 -0
  18. package/src/cli/registry.help.test.ts +18 -0
  19. package/src/cli/registry.ts +0 -4
  20. package/src/cli/safe-defaults.test.ts +3 -3
  21. package/src/config/define-config.test.ts +61 -0
  22. package/src/config/index.ts +89 -6
  23. package/src/config/resolve-driver.test.ts +30 -0
  24. package/src/console/server/app.ts +2 -1
  25. package/src/console/server/flows.ts +4 -1
  26. package/src/console/server/index.ts +5 -0
  27. package/src/console/server/operator-db.test.ts +213 -0
  28. package/src/console/server/operator-db.ts +470 -0
  29. package/src/console/server/plugin.ts +20 -0
  30. package/src/console/server/serve.ts +47 -1
  31. package/src/console/server/state.ts +25 -2
  32. package/src/console/ui/dist/assets/{index-B71Yl_SS.js → index-Dy4jht9P.js} +3 -3
  33. package/src/console/ui/dist/assets/{panel-overview-Bd48d9km.js → panel-overview-Dt_AeXgd.js} +1 -1
  34. package/src/console/ui/dist/assets/{panel-runs-BwsWqKeB.js → panel-runs-DGstFHeq.js} +1 -1
  35. package/src/console/ui/dist/assets/{panel-signals-9najbZY2.js → panel-signals-DzEa2Fnt.js} +1 -1
  36. package/src/console/ui/dist/assets/{panel-store-OHkP2pDp.js → panel-store-BJkbNgxx.js} +1 -1
  37. package/src/console/ui/dist/assets/{panel-traces-tn2JoY8U.js → panel-traces-BaRVO3gM.js} +1 -1
  38. package/src/console/ui/dist/assets/style-C8MxEWPd.css +3 -0
  39. package/src/console/ui/dist/favicon.svg +7 -0
  40. package/src/console/ui/dist/index.html +3 -2
  41. package/src/console/ui/shell/App.tsx +44 -4
  42. package/src/console/ui/shell/components/oke-logo.tsx +40 -0
  43. package/src/console/ui/shell/index.html +1 -0
  44. package/src/console/ui/shell/layout/Shell.tsx +2 -3
  45. package/src/console/ui/shell/panels/overview/OverviewPanel.tsx +8 -0
  46. package/src/console/ui/shell/public/favicon.svg +7 -0
  47. package/src/console/ui/shell/setup/Wizard.tsx +5 -2
  48. package/src/docker/compose.ts +45 -14
  49. package/src/docker/derive.ts +32 -10
  50. package/src/docker/docker.test.ts +28 -4
  51. package/src/docker/dockerfile.integration.test.ts +3 -1
  52. package/src/docker/index.ts +10 -0
  53. package/src/docker/stack-id.test.ts +86 -0
  54. package/src/docker/stack-id.ts +108 -0
  55. package/src/docker/stack.integration.test.ts +17 -8
  56. package/src/docker/types.ts +20 -1
  57. package/src/kernel/app.ts +39 -8
  58. package/src/kernel/boot-bind/store.test.ts +60 -0
  59. package/src/kernel/boot-bind/store.ts +142 -12
  60. package/src/kernel/boot.ts +28 -4
  61. package/src/mcp/server.ts +99 -57
  62. package/src/runtime/dev-request-log.test.ts +33 -0
  63. package/src/runtime/dev-request-log.ts +130 -0
  64. package/src/term.test.ts +98 -5
  65. package/src/term.ts +287 -6
  66. 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.store.sql.yml · compose.store.kv.yml ·
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 also boot the real infra stack (generated compose)
255
- oke dev -s store.sql,signal # partial: only these roles get real backends
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.store.sql.yml · compose.store.kv.yml ·
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.
@@ -43,7 +43,16 @@ import { defineConfig } from "okengine/config";
43
43
 
44
44
  export default defineConfig({
45
45
  drivers: {
46
- store: { sql: { dev: "sqlite", test: "memory", prod: "postgres" } },
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 also boot the real infra stack (generated compose)
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.store.sql.yml · compose.store.kv.yml ·
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: { sql: { dev: "sqlite", test: "memory", prod: "postgres" } },
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 also boot the real infra stack (generated compose)
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.store.sql.yml · compose.store.kv.yml ·
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.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:manifest": "bun test --coverage src/manifest",
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
- "build:console": "bunx vite build --config src/console/ui/vite.config.ts",
38
- "prepack": "bun run build:console",
39
- "bench:cold-start": "bun test ./src/runtime/cold-start.bench.ts",
40
- "bench:client": "bun test src/client/budget.test.ts",
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:doctor-diff": "bun src/cli/doctor-diff-examples.ts",
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
- "test:skyport-docker": "bun test examples/skyport/tests/docker.test.ts",
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:all": "bun run scripts/publish.ts",
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 { formatAppReadyLine } from "../term.ts";
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
- process.stdout.write(
64
- formatAppReadyLine(
65
- `http://${formatHostForUrl(hostname)}:${handle.port}`,
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` also boots generated compose.
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 { loadManifest, loadOkeConfig, resolveImages } from "./load-config.ts";
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 appPort = options.appPort ?? Number(Bun.env.PORT ?? APP_PORT);
187
- const consolePort = options.consolePort ?? CONSOLE_PORT;
188
- const mcpPort = options.mcpPort ?? MCP_PORT;
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
- const loaded = await loadOkeConfig(cwd);
214
- images = resolveImages(loaded.config);
215
- } catch (err) {
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: "dev",
298
+ app: appSlug,
233
299
  prod: false,
234
- ...(options.credentials ? { credentials: options.credentials } : {}),
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(cwd, "compose.override.yml"),
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, cwd, { writeStackEnv: true });
248
- // Ensure .env.stack is loaded for the app process
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
- await up(composeFiles, cwd);
266
- write(formatStatusLine(`stack up (${stackRoles.join(", ")})`));
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
- write(formatDevBanner());
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
- * Load `oke.manifest.json` / `manifest.oke.json` when present.
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
- return loadManifest(path);
637
+ fromDisk = await loadManifest(path);
638
+ break;
467
639
  }
468
640
  }
469
- return undefined;
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
- const proc = Bun.spawn(["bun", "--hot", runner], {
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 generated compose (partial roles: -s store.sql,signal).
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
  }