okengine 0.2.8 → 0.3.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 (180) hide show
  1. package/AGENTS.md +17 -15
  2. package/README.md +59 -31
  3. package/package.json +11 -16
  4. package/site/content/docs/ai/llms-txt.mdx +54 -0
  5. package/site/content/docs/ai/mcp.mdx +123 -0
  6. package/site/content/docs/ai/meta.json +5 -0
  7. package/site/content/docs/ai/skills.mdx +53 -0
  8. package/site/content/docs/console/access.mdx +29 -0
  9. package/site/content/docs/console/ai.mdx +35 -0
  10. package/site/content/docs/console/architecture.mdx +35 -0
  11. package/site/content/docs/console/channels.mdx +37 -0
  12. package/site/content/docs/console/clock.mdx +31 -0
  13. package/site/content/docs/console/flows.mdx +31 -0
  14. package/site/content/docs/console/gates.mdx +35 -0
  15. package/site/content/docs/console/manifest-diff.mdx +34 -0
  16. package/site/content/docs/console/meta.json +23 -0
  17. package/site/content/docs/console/overview.mdx +40 -0
  18. package/site/content/docs/console/plugins.mdx +41 -0
  19. package/site/content/docs/console/privacy.mdx +32 -0
  20. package/site/content/docs/console/runs.mdx +40 -0
  21. package/site/content/docs/console/signals.mdx +31 -0
  22. package/site/content/docs/console/store.mdx +32 -0
  23. package/site/content/docs/console/tenancy.mdx +32 -0
  24. package/site/content/docs/console/traces.mdx +34 -0
  25. package/site/content/docs/console/vault.mdx +37 -0
  26. package/site/content/docs/elements/ai.mdx +180 -0
  27. package/site/content/docs/elements/channel.mdx +167 -0
  28. package/site/content/docs/elements/clock.mdx +182 -0
  29. package/site/content/docs/elements/flow.mdx +288 -0
  30. package/site/content/docs/elements/gate.mdx +171 -0
  31. package/site/content/docs/elements/meta.json +5 -0
  32. package/site/content/docs/elements/signal.mdx +171 -0
  33. package/site/content/docs/elements/store.mdx +320 -0
  34. package/site/content/docs/elements/vault.mdx +263 -0
  35. package/site/content/docs/get-started/basic-usage.mdx +124 -0
  36. package/site/content/docs/get-started/comparison.mdx +65 -0
  37. package/site/content/docs/get-started/installation.mdx +113 -0
  38. package/site/content/docs/get-started/introduction.mdx +123 -0
  39. package/site/content/docs/get-started/meta.json +5 -0
  40. package/site/content/docs/index.mdx +63 -0
  41. package/site/content/docs/meta.json +5 -0
  42. package/site/content/docs/plugins/compression.mdx +60 -0
  43. package/site/content/docs/plugins/cors.mdx +98 -0
  44. package/site/content/docs/plugins/csrf.mdx +96 -0
  45. package/site/content/docs/plugins/ip-allowlist.mdx +93 -0
  46. package/site/content/docs/plugins/maintenance-mode.mdx +101 -0
  47. package/site/content/docs/plugins/meta.json +15 -0
  48. package/site/content/docs/plugins/security-headers.mdx +136 -0
  49. package/site/content/docs/reference/cli.md +101 -0
  50. package/site/content/docs/reference/configuration.mdx +159 -0
  51. package/site/content/docs/reference/environment-variables.mdx +87 -0
  52. package/site/content/docs/reference/errors.mdx +80 -0
  53. package/site/content/docs/reference/fx.mdx +117 -0
  54. package/site/content/docs/reference/meta.json +5 -0
  55. package/site/content/docs/reference/plugins.mdx +249 -0
  56. package/site/content/docs/reference/security.md +63 -0
  57. package/src/auth/auth.test.ts +3 -0
  58. package/src/cli/ask-dev-mode.ts +1 -1
  59. package/src/cli/db.ts +87 -17
  60. package/src/cli/dev-db-push.test.ts +32 -2
  61. package/src/cli/dev-schema-sync.test.ts +66 -0
  62. package/src/cli/dev-schema-sync.ts +139 -0
  63. package/src/cli/dev.test.ts +123 -1
  64. package/src/cli/dev.ts +150 -8
  65. package/src/cli/doc-staleness.test.ts +4 -4
  66. package/src/cli/docker-cli.test.ts +20 -0
  67. package/src/cli/docker.ts +10 -0
  68. package/src/cli/drizzle-env.test.ts +67 -0
  69. package/src/cli/drizzle-env.ts +78 -0
  70. package/src/cli/ensure-drizzle-config.ts +50 -0
  71. package/src/cli/hero-meta.test.ts +1 -1
  72. package/src/cli/load-config.ts +6 -0
  73. package/src/cli/mode.ts +24 -4
  74. package/src/cli/openbao-bootstrap.test.ts +149 -0
  75. package/src/cli/openbao-bootstrap.ts +314 -0
  76. package/src/cli/openbao-restart.integration.test.ts +136 -0
  77. package/src/cli/ports.test.ts +7 -5
  78. package/src/cli/ports.ts +6 -2
  79. package/src/cli/resolve-dev-sql-env.test.ts +48 -0
  80. package/src/cli/resolve-dev-sql-env.ts +42 -0
  81. package/src/cli/stack.ts +7 -4
  82. package/src/cli/vault-cmd.ts +63 -0
  83. package/src/client/types.ts +7 -1
  84. package/src/compiler/extract.test.ts +40 -0
  85. package/src/compiler/extract.ts +123 -1
  86. package/src/compiler/fixtures/skyport/oke.config.ts +2 -2
  87. package/src/compiler/fixtures/skyport.expected.json +1 -1
  88. package/src/compiler/response.ts +12 -0
  89. package/src/config/define-config.test.ts +4 -6
  90. package/src/config/index.ts +2 -15
  91. package/src/console/server/app.ts +2 -0
  92. package/src/console/server/vault.ts +21 -6
  93. package/src/docker/compose.ts +112 -16
  94. package/src/docker/derive.ts +7 -1
  95. package/src/docker/docker.test.ts +103 -0
  96. package/src/docker/index.ts +11 -1
  97. package/src/docker/recipes/index.ts +3 -2
  98. package/src/docker/recipes/openbao.ts +47 -0
  99. package/src/docker/recipes/redis.ts +5 -1
  100. package/src/docker/recipes/rustfs.ts +2 -3
  101. package/src/docker/stack-id.test.ts +43 -8
  102. package/src/docker/stack-id.ts +99 -20
  103. package/src/docker/stack.ts +36 -4
  104. package/src/docker/types.ts +3 -0
  105. package/src/docs-origin.ts +4 -4
  106. package/src/drivers/drizzle-dialect.test.ts +20 -0
  107. package/src/drivers/drizzle-dialect.ts +37 -0
  108. package/src/drivers/index.ts +1 -2
  109. package/src/drivers/memory.ts +278 -39
  110. package/src/drivers/s3.ts +10 -1
  111. package/src/drivers/vault-driver-removal.test.ts +55 -0
  112. package/src/drivers/vault-openbao.test.ts +97 -0
  113. package/src/drivers/vault-openbao.ts +102 -35
  114. package/src/drivers/vault-types.ts +3 -10
  115. package/src/elements/store/declare.ts +4 -1
  116. package/src/elements/store/resource-list-docs.fixture.ts +56 -0
  117. package/src/elements/store/resource-list-docs.test.ts +79 -0
  118. package/src/elements/store/resource.test.ts +253 -0
  119. package/src/elements/store/resource.ts +786 -0
  120. package/src/elements/store/sql-condition.test.ts +132 -0
  121. package/src/elements/store/sql-condition.ts +284 -46
  122. package/src/elements/store/sql-session.test.ts +86 -1
  123. package/src/elements/store/sql-session.ts +187 -27
  124. package/src/elements/store/table.ts +34 -4
  125. package/src/elements/store.ts +16 -0
  126. package/src/elements/vault/runtime.ts +1 -1
  127. package/src/elements/vault.test.ts +1 -28
  128. package/src/elements/vault.ts +1 -1
  129. package/src/kernel/app.ts +59 -25
  130. package/src/kernel/boot-bind/channel.test.ts +60 -0
  131. package/src/kernel/boot-bind/channel.ts +64 -2
  132. package/src/kernel/boot-bind/store.test.ts +10 -1
  133. package/src/kernel/boot-bind/store.ts +49 -2
  134. package/src/kernel/boot.test.ts +0 -1
  135. package/src/kernel/boot.ts +1 -1
  136. package/src/kernel/edge.test.ts +68 -0
  137. package/src/kernel/errors.registry.test.ts +1 -1
  138. package/src/kernel/flow.ts +8 -0
  139. package/src/kernel/fx.test.ts +23 -3
  140. package/src/kernel/fx.ts +115 -18
  141. package/src/kernel/hooks.test.ts +33 -0
  142. package/src/kernel/hooks.ts +22 -0
  143. package/src/kernel/index.ts +7 -0
  144. package/src/kernel/on.ts +44 -3
  145. package/src/kernel/plugin.ts +33 -3
  146. package/src/kernel/registry-isolation.test.ts +74 -0
  147. package/src/kernel/registry.ts +22 -1
  148. package/src/kernel/triggers.ts +59 -0
  149. package/src/manifest/fixtures/skyport.excerpt.json +1 -1
  150. package/src/manifest/fixtures/skyport.manifest.json +1 -1
  151. package/src/manifest/index.ts +1 -1
  152. package/src/manifest/types.ts +1 -1
  153. package/src/manifest/validate.ts +2 -2
  154. package/src/plugins/compression.test.ts +127 -0
  155. package/src/plugins/compression.ts +94 -0
  156. package/src/plugins/config-source.test.ts +204 -0
  157. package/src/plugins/config-source.ts +209 -0
  158. package/src/plugins/cors.test.ts +172 -0
  159. package/src/plugins/cors.ts +153 -0
  160. package/src/plugins/csrf.test.ts +102 -0
  161. package/src/plugins/csrf.ts +86 -0
  162. package/src/plugins/headers.ts +54 -0
  163. package/src/plugins/index.ts +26 -0
  164. package/src/plugins/ip-allowlist.test.ts +139 -0
  165. package/src/plugins/ip-allowlist.ts +129 -0
  166. package/src/plugins/maintenance-mode.test.ts +91 -0
  167. package/src/plugins/maintenance-mode.ts +85 -0
  168. package/src/plugins/security-headers.test.ts +243 -0
  169. package/src/plugins/security-headers.ts +255 -0
  170. package/src/release/measure.ts +1 -2
  171. package/src/test/create-test-app.ts +14 -2
  172. package/docs/spec/console.md +0 -762
  173. package/docs/spec/example.md +0 -1374
  174. package/docs/spec/four-applications.md +0 -1376
  175. package/docs/spec/unified-theory.md +0 -498
  176. package/src/cli/doc-drift.test.ts +0 -147
  177. package/src/cli/doc-drift.ts +0 -401
  178. package/src/cli/doctor-diff-examples.ts +0 -90
  179. package/src/drivers/vault-sops.ts +0 -246
  180. /package/{spec/manifest.v1.schema.json → manifest.v1.schema.json} +0 -0
@@ -6,7 +6,7 @@ import { afterEach, describe, expect, test } from "bun:test";
6
6
  import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
7
7
  import { tmpdir } from "node:os";
8
8
  import { join } from "node:path";
9
- import { runDev, type DevSession } from "./dev.ts";
9
+ import { runDev, type DevSession, type DevWatchFn } from "./dev.ts";
10
10
 
11
11
  const sessions: DevSession[] = [];
12
12
 
@@ -14,6 +14,30 @@ afterEach(() => {
14
14
  for (const s of sessions.splice(0)) s.stop();
15
15
  });
16
16
 
17
+ async function waitFor(predicate: () => boolean, timeoutMs = 3_000): Promise<void> {
18
+ const deadline = Date.now() + timeoutMs;
19
+ while (!predicate() && Date.now() < deadline) {
20
+ await Bun.sleep(20);
21
+ }
22
+ }
23
+
24
+ function controlledWatcher(): {
25
+ watchFs: DevWatchFn;
26
+ change(filename: string): void;
27
+ } {
28
+ let listener: Parameters<DevWatchFn>[2] | undefined;
29
+ return {
30
+ watchFs: (_path, _options, next) => {
31
+ listener = next;
32
+ return { close() {} };
33
+ },
34
+ change(filename) {
35
+ if (!listener) throw new Error("watcher not started");
36
+ listener("change", filename);
37
+ },
38
+ };
39
+ }
40
+
17
41
  async function scaffoldProject(): Promise<string> {
18
42
  const dir = await mkdtemp(join(tmpdir(), "oke-dev-db-"));
19
43
  await mkdir(join(dir, "src"), { recursive: true });
@@ -51,6 +75,7 @@ describe("oke dev db auto-push", () => {
51
75
  test("schema change triggers dbPush when auto-push is on", async () => {
52
76
  const cwd = await scaffoldProject();
53
77
  const pushes: string[] = [];
78
+ const watcher = controlledWatcher();
54
79
  let resolveReady!: () => void;
55
80
  const ready = new Promise<void>((r) => {
56
81
  resolveReady = r;
@@ -88,6 +113,7 @@ describe("oke dev db auto-push", () => {
88
113
  onDbAutoPush: () => {
89
114
  resolveReady();
90
115
  },
116
+ watchFs: watcher.watchFs,
91
117
  onReady: async (session) => {
92
118
  sessions.push(session);
93
119
  },
@@ -108,13 +134,15 @@ export const entries = sqliteTable("entries", {
108
134
  });
109
135
  `,
110
136
  );
111
- await Bun.sleep(500);
137
+ watcher.change("schema.ts");
138
+ await waitFor(() => pushes.length > before);
112
139
  expect(pushes.length).toBeGreaterThan(before);
113
140
  });
114
141
 
115
142
  test("--no-db-push / noDbPush never calls dbPush", async () => {
116
143
  const cwd = await scaffoldProject();
117
144
  const pushes: string[] = [];
145
+ const watcher = controlledWatcher();
118
146
 
119
147
  const result = await runDev({
120
148
  cwd,
@@ -145,6 +173,7 @@ export const entries = sqliteTable("entries", {
145
173
  pushes.push("push");
146
174
  return 0;
147
175
  },
176
+ watchFs: watcher.watchFs,
148
177
  onReady: async (session) => {
149
178
  sessions.push(session);
150
179
  },
@@ -157,6 +186,7 @@ export const entries = sqliteTable("entries", {
157
186
  export const entries = sqliteTable("entries", { id: text("id").primaryKey(), x: text("x") });
158
187
  `,
159
188
  );
189
+ watcher.change("schema.ts");
160
190
  await Bun.sleep(500);
161
191
  expect(pushes).toHaveLength(0);
162
192
  });
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Mode-switch schema sync pipeline — ensure → emit → push, no data copy.
3
+ */
4
+
5
+ import { afterEach, describe, expect, test } from "bun:test";
6
+ import { mkdtemp, rm } from "node:fs/promises";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import type { ConfigEnv } from "../config/index.ts";
10
+ import { sqlDialectForEnv, syncDevSchema } from "./dev-schema-sync.ts";
11
+
12
+ const dirs: string[] = [];
13
+
14
+ afterEach(async () => {
15
+ await Promise.all(dirs.splice(0).map((d) => rm(d, { recursive: true, force: true })));
16
+ });
17
+
18
+ async function tempDir(): Promise<string> {
19
+ const dir = await mkdtemp(join(tmpdir(), "oke-sync-"));
20
+ dirs.push(dir);
21
+ return dir;
22
+ }
23
+
24
+ describe("sqlDialectForEnv", () => {
25
+ test("local → sqlite, docker → postgresql", async () => {
26
+ const config = {
27
+ drivers: {
28
+ store: { sql: { local: "sqlite", docker: "postgres", prod: "postgres", test: "memory" } },
29
+ },
30
+ } as never;
31
+ expect(sqlDialectForEnv(config, "local").dialect).toBe("sqlite");
32
+ expect(sqlDialectForEnv(config, "docker").dialect).toBe("postgresql");
33
+ });
34
+
35
+ test("missing config → sqlite default", () => {
36
+ expect(sqlDialectForEnv(null, "local").dialect).toBe("sqlite");
37
+ });
38
+ });
39
+
40
+ describe("syncDevSchema", () => {
41
+ test("local: ensures drizzle config, emits, pushes with env", async () => {
42
+ const dir = await tempDir();
43
+ const calls: { cwd: string; env: ConfigEnv }[] = [];
44
+ const result = await syncDevSchema(dir, "local", {
45
+ write: () => {},
46
+ pushFn: async (cwd, env) => {
47
+ calls.push({ cwd, env });
48
+ return 0;
49
+ },
50
+ });
51
+ expect(result.dialect).toBe("sqlite");
52
+ expect(result.pushed).toBe(true);
53
+ expect(calls).toEqual([{ cwd: dir, env: "local" }]);
54
+ expect(await Bun.file(join(dir, "drizzle.config.ts")).exists()).toBe(true);
55
+ });
56
+
57
+ test("push failure surfaces code without throwing", async () => {
58
+ const dir = await tempDir();
59
+ const result = await syncDevSchema(dir, "local", {
60
+ write: () => {},
61
+ pushFn: async () => 1,
62
+ });
63
+ expect(result.pushed).toBe(false);
64
+ expect(result.code).toBe(1);
65
+ });
66
+ });
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Shared local↔docker schema sync pipeline for `oke mode` and `oke dev`
3
+ * one-shot start. Ensures artefacts, emits for the active dialect, then
4
+ * pushes via drizzle-kit. Data planes stay isolated — no row copying.
5
+ */
6
+
7
+ import { resolve } from "node:path";
8
+ import { resolveDriverId, type ConfigEnv, type OkeConfig } from "../config/index.ts";
9
+ import {
10
+ DEFAULT_DOCKER_DIR,
11
+ deriveInfrastructure,
12
+ loadExistingStackControls,
13
+ loadExistingStackCredentials,
14
+ stackAppSlug,
15
+ stackInstanceId,
16
+ writeDerivedFiles,
17
+ } from "../docker/index.ts";
18
+ import { drizzleDialectFromSqlDriver } from "../drivers/drizzle-dialect.ts";
19
+ import { ensureDrizzleConfig } from "./ensure-drizzle-config.ts";
20
+ import { runDb } from "./db.ts";
21
+ import { loadOkeConfig, resolveImages } from "./load-config.ts";
22
+
23
+ /** Options for {@link syncDevSchema}. */
24
+ export interface DevSchemaSyncOptions {
25
+ /** Write output (default: stdout). */
26
+ readonly write?: (text: string) => void;
27
+ /** Skip emitting `schema.generated.ts` (tests). */
28
+ readonly skipEmit?: boolean;
29
+ /** Injectable push (tests). Default: `runDb("push", ...)`. */
30
+ readonly pushFn?: (cwd: string, env: ConfigEnv) => Promise<number>;
31
+ /** Derive docker infra without running compose (tests). */
32
+ readonly dryRun?: boolean;
33
+ }
34
+
35
+ /** Outcome of {@link syncDevSchema}. */
36
+ export interface DevSchemaSyncResult {
37
+ readonly env: ConfigEnv;
38
+ readonly dialect: "sqlite" | "postgresql";
39
+ readonly pushed: boolean;
40
+ readonly code: number;
41
+ }
42
+
43
+ /**
44
+ * Resolve the SQL driver id + drizzle dialect for an env from config.
45
+ *
46
+ * @param config - Loaded oke config
47
+ * @param env - Active env
48
+ */
49
+ export function sqlDialectForEnv(
50
+ config: OkeConfig | null | undefined,
51
+ env: ConfigEnv,
52
+ ): { readonly driverId: string; readonly dialect: "sqlite" | "postgresql" } {
53
+ const driverId = resolveDriverId(config?.drivers?.store?.sql, env) ?? "sqlite";
54
+ if (driverId !== "sqlite" && driverId !== "postgres") {
55
+ throw new Error(
56
+ `store.sql driver "${driverId}" is not supported by drizzle-kit (sqlite | postgres)`,
57
+ );
58
+ }
59
+ return { driverId, dialect: drizzleDialectFromSqlDriver(driverId) };
60
+ }
61
+
62
+ /**
63
+ * Ensure docker compose artefacts + credentials exist. Fails loudly with a
64
+ * clear hint when images are not configured (no silent retry loop).
65
+ *
66
+ * @param cwd - Project root
67
+ * @param config - Loaded oke config
68
+ * @param write - Output
69
+ */
70
+ async function ensureDockerStack(
71
+ cwd: string,
72
+ config: OkeConfig,
73
+ write: (text: string) => void,
74
+ ): Promise<void> {
75
+ const images = resolveImages(config);
76
+ if (Object.keys(images).length === 0) {
77
+ throw new Error(
78
+ "docker mode: no images configured — set `images` in oke.config.ts (or prod drivers postgres/redis for defaults)",
79
+ );
80
+ }
81
+ const roles = Object.keys(images);
82
+ const instanceId = stackInstanceId(cwd);
83
+ const appSlug = stackAppSlug(cwd);
84
+ const credentials = await loadExistingStackCredentials(cwd, roles);
85
+ const controls = await loadExistingStackControls(cwd);
86
+ const derived = deriveInfrastructure({
87
+ images,
88
+ app: appSlug,
89
+ prod: false,
90
+ includeApp: false,
91
+ composeDir: DEFAULT_DOCKER_DIR,
92
+ instanceId,
93
+ ...(credentials ? { credentials } : {}),
94
+ ...(controls ? { controls } : {}),
95
+ host: "127.0.0.1",
96
+ });
97
+ await writeDerivedFiles(derived, resolve(cwd, DEFAULT_DOCKER_DIR), { writeStackEnv: true });
98
+ write(
99
+ "oke: docker compose env ready under docker/.env.docker (start containers with `oke dev -d` or docker compose up)\n",
100
+ );
101
+ }
102
+
103
+ /**
104
+ * Ensure artefacts, emit for the active dialect, and push schema for an env.
105
+ * Returns a one-line summary outcome. Docker mode also ensures compose env
106
+ * exists (writes `docker/.env.docker`) before pushing to the live DB.
107
+ *
108
+ * @param cwd - Project root
109
+ * @param env - Target env (`local` | `docker`)
110
+ * @param options - Injectables
111
+ */
112
+ export async function syncDevSchema(
113
+ cwd: string,
114
+ env: ConfigEnv,
115
+ options: DevSchemaSyncOptions = {},
116
+ ): Promise<DevSchemaSyncResult> {
117
+ const write = options.write ?? ((t) => process.stdout.write(t));
118
+ const loaded = await loadOkeConfig(cwd).catch(() => null);
119
+ const config = loaded?.config;
120
+
121
+ await ensureDrizzleConfig(cwd);
122
+
123
+ const { dialect } = sqlDialectForEnv(config, env);
124
+
125
+ if (env === "docker") {
126
+ if (!config) {
127
+ throw new Error("docker mode: oke.config.ts not found");
128
+ }
129
+ await ensureDockerStack(cwd, config, write);
130
+ }
131
+
132
+ const push =
133
+ options.pushFn ??
134
+ (async (projectCwd, envArg) =>
135
+ runDb("push", { cwd: projectCwd, env: envArg, write, skipEmit: options.skipEmit }));
136
+
137
+ const code = await push(cwd, env);
138
+ return { env, dialect, pushed: code === 0, code };
139
+ }
@@ -272,6 +272,121 @@ describe("oke dev MCP live wiring", () => {
272
272
  });
273
273
  });
274
274
 
275
+ describe("oke dev docs MCP", () => {
276
+ let session: DevSession | undefined;
277
+
278
+ afterEach(() => {
279
+ session?.stop();
280
+ session = undefined;
281
+ });
282
+
283
+ test("boots the real docs MCP on :6536 next to the runtime MCP — no auth, search works", async () => {
284
+ const dir = await mkdtemp(join(tmpdir(), "oke-dev-docs-mcp-"));
285
+ await Bun.write(join(dir, "src/app.ts"), "export {}\n");
286
+
287
+ const result = await runDev({
288
+ stdinIsTTY: false,
289
+ cwd: dir,
290
+ silentClaim: true,
291
+ keepAlive: false,
292
+ appPort: 0,
293
+ consolePort: 0,
294
+ mcpPort: 0,
295
+ docsMcpPort: 0,
296
+ startApp: async () => ({ stop() {} }),
297
+ regenClient: async () => {},
298
+ write: () => {},
299
+ // Docs MCP must boot even when Console state is absent (headless).
300
+ serveConsole: async () => ({ stop() {} }),
301
+ serveMcp: async () => ({ stop() {} }),
302
+ });
303
+
304
+ expect(result.code).toBe(0);
305
+ session = result.session;
306
+ expect(session).toBeDefined();
307
+ if (!session) return;
308
+
309
+ expect(session.docsMcpUrl).toBeTruthy();
310
+ const docsBase = session.docsMcpUrl!.origin;
311
+ const host = `127.0.0.1:${session.docsMcpPort}`;
312
+
313
+ const health = await fetch(`${docsBase}/health`, { headers: { host } });
314
+ expect(health.status).toBe(200);
315
+ expect(await health.json()).toEqual({ ok: true, surface: "docs-mcp" });
316
+
317
+ // No Authorization header — docs MCP serves public documentation.
318
+ const listRes = await fetch(`${docsBase}/mcp`, {
319
+ method: "POST",
320
+ headers: { host, "content-type": "application/json" },
321
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} }),
322
+ });
323
+ expect(listRes.status).toBe(200);
324
+ const listBody = (await listRes.json()) as {
325
+ result: { tools: { name: string }[] };
326
+ };
327
+ const toolNames = listBody.result.tools.map((t) => t.name);
328
+ expect(toolNames).toContain("oke.docs.search");
329
+ expect(toolNames).toContain("oke.docs.get");
330
+
331
+ const searchRes = await fetch(`${docsBase}/mcp`, {
332
+ method: "POST",
333
+ headers: { host, "content-type": "application/json" },
334
+ body: JSON.stringify({
335
+ jsonrpc: "2.0",
336
+ id: 2,
337
+ method: "tools/call",
338
+ params: { name: "oke.docs.search", arguments: { query: "vault" } },
339
+ }),
340
+ });
341
+ expect(searchRes.status).toBe(200);
342
+ const searchBody = (await searchRes.json()) as {
343
+ result: {
344
+ structuredContent: { kind: string; content: { hits: { slug: string }[] } };
345
+ };
346
+ };
347
+ expect(searchBody.result.structuredContent.kind).toBe(MCP_DATA_KIND);
348
+ expect(searchBody.result.structuredContent.content.hits.length).toBeGreaterThan(0);
349
+
350
+ session.stop();
351
+ session = undefined;
352
+ await expect(fetch(`${docsBase}/health`, { headers: { host } })).rejects.toThrow();
353
+ }, 60_000);
354
+
355
+ test("skips docs MCP without killing oke dev when the surface fails to boot", async () => {
356
+ const dir = await mkdtemp(join(tmpdir(), "oke-dev-docs-skip-"));
357
+ await Bun.write(join(dir, "src/app.ts"), "export {}\n");
358
+
359
+ const writes: string[] = [];
360
+ const result = await runDev({
361
+ stdinIsTTY: false,
362
+ cwd: dir,
363
+ silentClaim: true,
364
+ keepAlive: false,
365
+ appPort: 0,
366
+ consolePort: 0,
367
+ mcpPort: 0,
368
+ docsMcpPort: 0,
369
+ startApp: async () => ({ stop() {} }),
370
+ regenClient: async () => {},
371
+ write: (t) => {
372
+ writes.push(t);
373
+ },
374
+ serveConsole: async () => ({ stop() {} }),
375
+ serveMcp: async () => ({ stop() {} }),
376
+ serveDocsMcp: async () => {
377
+ throw new Error("docs content missing");
378
+ },
379
+ });
380
+
381
+ expect(result.code).toBe(0);
382
+ session = result.session;
383
+ expect(session).toBeDefined();
384
+ if (!session) return;
385
+ expect(session.docsMcpUrl).toBeNull();
386
+ expect(writes.join("")).toContain("Docs MCP skipped");
387
+ }, 60_000);
388
+ });
389
+
275
390
  /**
276
391
  * Write a minimal HTTP flow app whose `do` returns `{ version }`.
277
392
  *
@@ -311,7 +426,13 @@ export const app = oke({ name: "dev-hot", env: "test" }).adopt({ ping });
311
426
  /** Stub Console + MCP so boot/hot tests isolate the app child. */
312
427
  function stubSurfaces(): Pick<
313
428
  DevOptions,
314
- "serveConsole" | "serveMcp" | "regenClient" | "write" | "silentClaim" | "keepAlive"
429
+ | "serveConsole"
430
+ | "serveMcp"
431
+ | "serveDocsMcp"
432
+ | "regenClient"
433
+ | "write"
434
+ | "silentClaim"
435
+ | "keepAlive"
315
436
  > {
316
437
  return {
317
438
  silentClaim: true,
@@ -320,6 +441,7 @@ function stubSurfaces(): Pick<
320
441
  write: () => {},
321
442
  serveConsole: async () => ({ stop() {} }),
322
443
  serveMcp: async () => ({ stop() {} }),
444
+ serveDocsMcp: async () => ({ stop() {} }),
323
445
  };
324
446
  }
325
447