okengine 0.2.3 → 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 (42) 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 +136 -17
  7. package/src/cli/docker-cli.test.ts +10 -8
  8. package/src/cli/docker.ts +18 -7
  9. package/src/cli/hero-meta.test.ts +85 -0
  10. package/src/cli/hero-meta.ts +252 -0
  11. package/src/cli/load-config.images.test.ts +51 -0
  12. package/src/cli/load-config.ts +84 -6
  13. package/src/config/define-config.test.ts +61 -0
  14. package/src/config/index.ts +89 -6
  15. package/src/config/resolve-driver.test.ts +30 -0
  16. package/src/console/server/flows.ts +3 -1
  17. package/src/console/server/operator-db.test.ts +140 -2
  18. package/src/console/server/operator-db.ts +147 -1
  19. package/src/console/server/plugin.ts +20 -0
  20. package/src/console/server/serve.ts +8 -1
  21. package/src/console/server/state.ts +12 -1
  22. package/src/console/ui/dist/assets/{index-Bnf_3Hei.js → index-Dy4jht9P.js} +1 -1
  23. package/src/console/ui/dist/index.html +1 -1
  24. package/src/console/ui/shell/App.tsx +10 -2
  25. package/src/docker/compose.ts +45 -14
  26. package/src/docker/derive.ts +29 -9
  27. package/src/docker/docker.test.ts +28 -4
  28. package/src/docker/dockerfile.integration.test.ts +2 -0
  29. package/src/docker/index.ts +10 -0
  30. package/src/docker/stack-id.test.ts +86 -0
  31. package/src/docker/stack-id.ts +108 -0
  32. package/src/docker/stack.integration.test.ts +16 -7
  33. package/src/docker/types.ts +20 -1
  34. package/src/kernel/app.ts +39 -8
  35. package/src/kernel/boot-bind/store.test.ts +60 -0
  36. package/src/kernel/boot-bind/store.ts +142 -12
  37. package/src/kernel/boot.ts +28 -4
  38. package/src/mcp/server.ts +99 -57
  39. package/src/runtime/dev-request-log.test.ts +33 -0
  40. package/src/runtime/dev-request-log.ts +130 -0
  41. package/src/term.test.ts +98 -5
  42. 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.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.3",
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,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 { 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";
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
- const loaded = await loadOkeConfig(cwd);
233
- images = resolveImages(loaded.config);
234
- } catch (err) {
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: "dev",
298
+ app: appSlug,
252
299
  prod: false,
253
- ...(options.credentials ? { credentials: options.credentials } : {}),
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(cwd, "compose.override.yml"),
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, cwd, { writeStackEnv: true });
267
- // 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.
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
- await up(composeFiles, cwd);
285
- 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
+ );
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
- 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
+ );
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
- 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], {
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 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.
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 without credentials in YAML", async () => {
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(await Bun.file(join(dir, "compose.store.sql.yml")).exists()).toBe(
44
- true,
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(logs.join("")).toContain("compose.override.yml");
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 outDir = resolve(cwd, options.outDir ?? ".");
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)\n`);
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
- "(compose.override.yml is layer 4 — user-owned, never written by oke)\n",
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 .] [--config|-c oke.config.ts]
123
+ console.log(`oke docker [--prod|-p] [--out|-o docker] [--config|-c oke.config.ts]
113
124
 
114
- Derive Dockerfile + compose.<role>.yml files.
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;