create-oke 0.18.5 → 0.19.0

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 (62) hide show
  1. package/README.md +1 -1
  2. package/package.json +3 -3
  3. package/src/agents-md.ts +6 -4
  4. package/src/ai-setup/apply.test.ts +278 -0
  5. package/src/ai-setup/apply.ts +430 -52
  6. package/src/ai-setup/catalog.ts +250 -1343
  7. package/src/ai-setup/from-pref.ts +3 -71
  8. package/src/ai-setup/prompts.ts +60 -443
  9. package/src/cli.test.ts +21 -15
  10. package/src/cli.ts +20 -11
  11. package/src/create-defaults.test.ts +4 -4
  12. package/src/create-defaults.ts +3 -3
  13. package/src/customize-flow.test.ts +9 -15
  14. package/src/customize-flow.ts +59 -66
  15. package/src/drivers-catalog.ts +18 -23
  16. package/src/transform.test.ts +69 -10
  17. package/src/transform.ts +10 -35
  18. package/templates/advanced/.env.example +28 -11
  19. package/templates/advanced/.github/workflows/ci.yml +1 -1
  20. package/templates/advanced/oke.config.ts +3 -4
  21. package/templates/advanced/package.json +11 -10
  22. package/templates/advanced/src/app.ts +36 -1
  23. package/templates/advanced/src/core.ts +12 -11
  24. package/templates/advanced/src/db/schema.decl.ts +6 -2
  25. package/templates/advanced/src/db/seed/index.ts +4 -4
  26. package/templates/advanced/src/flows/main/route.ts +3 -2
  27. package/templates/advanced/src/flows/notes/[id]/archive.ts +5 -8
  28. package/templates/advanced/src/flows/notes/[id]/attach.ts +1 -4
  29. package/templates/advanced/src/flows/notes/[id]/get.ts +4 -7
  30. package/templates/advanced/src/flows/notes/[id]/summarize.ts +3 -4
  31. package/templates/advanced/src/flows/notes/create.ts +4 -6
  32. package/templates/advanced/src/flows/notes/digest.ts +6 -5
  33. package/templates/advanced/src/flows/notes/list.ts +4 -5
  34. package/templates/advanced/src/flows/notes/shapes.ts +13 -3
  35. package/templates/advanced/src/flows/notes/signals.ts +1 -2
  36. package/templates/advanced/src/vault.ts +138 -0
  37. package/templates/advanced/tests/advanced.test.ts +11 -8
  38. package/templates/advanced/web/src/App.css +7 -0
  39. package/templates/advanced/web/src/App.tsx +101 -1
  40. package/templates/advanced/web/src/client.ts +16 -3
  41. package/templates/advanced/web/vite.config.ts +1 -0
  42. package/templates/standard/.env.example +22 -11
  43. package/templates/standard/.github/workflows/ci.yml +1 -1
  44. package/templates/standard/oke.config.ts +3 -3
  45. package/templates/standard/package.json +11 -10
  46. package/templates/standard/src/app.ts +6 -1
  47. package/templates/standard/src/core.ts +10 -11
  48. package/templates/standard/src/db/schema.decl.ts +6 -2
  49. package/templates/standard/src/db/seed/index.ts +3 -3
  50. package/templates/standard/src/flows/main/route.ts +3 -2
  51. package/templates/standard/src/flows/notes/[id]/archive.ts +5 -8
  52. package/templates/standard/src/flows/notes/[id]/get.ts +4 -7
  53. package/templates/standard/src/flows/notes/create.ts +4 -6
  54. package/templates/standard/src/flows/notes/list.ts +4 -5
  55. package/templates/standard/src/flows/notes/shapes.ts +12 -2
  56. package/templates/standard/src/flows/notes/signals.ts +1 -2
  57. package/templates/standard/src/vault.ts +123 -0
  58. package/templates/standard/tests/standard.test.ts +3 -1
  59. package/templates/standard/web/src/client.ts +2 -2
  60. package/templates/standard/web/vite.config.ts +1 -0
  61. package/src/ai-setup/detect-ollama.ts +0 -228
  62. package/src/ai-setup/recommend.ts +0 -220
package/README.md CHANGED
@@ -64,4 +64,4 @@ Every scaffold writes an `AGENTS.md` so coding agents know the OKE contract
64
64
  | npm (CLI) | [`create-oke`](https://www.npmjs.com/package/create-oke) — use this with `bunx` |
65
65
  | JSR | [`@omqkhafi/create-oke`](https://jsr.io/@omqkhafi/create-oke) — same sources; prefer npm for the binary |
66
66
 
67
- Requires **Bun ≥ 1.3.14**. Docs: [oke.omqkhafi.dev](https://oke.omqkhafi.dev) · [handbook](https://oke.omqkhafi.dev/docs).
67
+ Requires **Bun ≥ 1.4.2**. Docs: [oke.omqkhafi.dev](https://oke.omqkhafi.dev) · [handbook](https://oke.omqkhafi.dev/docs).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-oke",
3
- "version": "0.18.5",
3
+ "version": "0.19.0",
4
4
  "description": "Scaffold an okengine app — bunx create-oke@latest <name>",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -25,13 +25,13 @@
25
25
  "test": "bun test src"
26
26
  },
27
27
  "dependencies": {
28
- "@clack/prompts": "^1.7.0"
28
+ "@clack/prompts": "^1.8.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "@types/bun": "latest",
32
32
  "typescript": "^7.0.2"
33
33
  },
34
34
  "engines": {
35
- "bun": ">=1.4.0"
35
+ "bun": ">=1.4.2"
36
36
  }
37
37
  }
package/src/agents-md.ts CHANGED
@@ -69,7 +69,8 @@ are inferred from what a Flow touches through it.
69
69
  |---|---|
70
70
  | **6530** | Backend |
71
71
  | **6533** | Console |
72
- | **6535** | MCP |
72
+ | **6535** | App MCP |
73
+ | **6536** | Docs MCP (read-only) |
73
74
 
74
75
  Mnemonic: O·K·E = 6·5·3.
75
76
 
@@ -81,7 +82,7 @@ oke dev # or: bun run dev
81
82
  bun run web # Vite SPA (proxies Flows to the app)
82
83
  \`\`\`
83
84
 
84
- App \`:6530\` · Console \`:6533\` · MCP \`:6535\`. Vite web is \`bun run web\`.
85
+ App \`:6530\` · Console \`:6533\` · App MCP \`:6535\` · Docs MCP \`:6536\`. Vite web is \`bun run web\`.
85
86
 
86
87
  ## Common mistakes
87
88
 
@@ -90,13 +91,14 @@ App \`:6530\` · Console \`:6533\` · MCP \`:6535\`. Vite web is \`bun run web\`
90
91
  - ❌ Inventing a ninth “element” or a parallel handler stack beside Flows
91
92
  - ✅ New capability = new **driver** on an existing element, or a new Flow
92
93
  - ❌ Untyped HTTP handlers that skip \`on\` / \`flow\` / contracts
93
- - ✅ \`on(http.get(), flow({ in, out, do }))\` — or \`http.get("/…")\` / \`flow("unit.export", {})\` when the folder is not the URL
94
+ - ✅ \`on(http.get({ in, out }), flow({ do }))\` — or \`http.get("/…", { in, out })\` / \`call("unit.export", { in, out, do })\` when the folder is not the URL
94
95
  - ❌ Returning \`{ items, count }\` from a list \`do\` (nests the pager inside \`data\`)
95
96
  - ✅ \`out: z.array(Item)\` + \`fx.json.withQuery(rows, input)\` — or any other \`out\` you declare
96
97
 
97
98
  ## Learn more
98
99
 
99
100
  - Handbook: ${handbook}
100
- - Get started: ${docsUrl("/docs/get-started/introduction")}
101
+ - Understand: ${docsUrl("/docs/understand/the-problem")}
102
+ - See it work: ${docsUrl("/docs/understand/the-anatomy")}
101
103
  `;
102
104
  }
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Unit tests for hybrid-search embed wiring in `oke ai setup` / create-oke.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
7
+ import { tmpdir } from "node:os";
8
+ import { join } from "node:path";
9
+ import {
10
+ DEFAULT_SEARCH_EMBED_DIMS,
11
+ applyAiSetup,
12
+ ensureAiApiKeyVaultSecret,
13
+ ensureAppStoreSearchEmbed,
14
+ ensureNotesBodyEmbed,
15
+ ensureSummarizeNotePrompt,
16
+ envNameToCamelBinding,
17
+ hasAiApiKeyVaultSecret,
18
+ isIncompleteAiSetup,
19
+ mergeAiApiKeyVaultSecret,
20
+ renderAiTs,
21
+ resolveAiCoreSource,
22
+ upsertEnv,
23
+ } from "./apply.ts";
24
+
25
+ describe("ensureNotesBodyEmbed", () => {
26
+ test("adds bare .embed() after .searchable() on body", () => {
27
+ const src = `
28
+ export const notes = store.schema.table("notes", {
29
+ id: field.id().primaryKey(),
30
+ title: field.text().searchable({ weight: 2 }).notNull(),
31
+ body: field.text().searchable().notNull(),
32
+ });
33
+ `;
34
+ expect(ensureNotesBodyEmbed(src)).toContain(
35
+ "body: field.text().searchable().embed().notNull()",
36
+ );
37
+ });
38
+
39
+ test("is idempotent when .embed() already present", () => {
40
+ const src = `body: field.text().searchable().embed().notNull(),`;
41
+ expect(ensureNotesBodyEmbed(src)).toBe(src);
42
+ });
43
+
44
+ test("adds .searchable().embed() when body has neither", () => {
45
+ const src = `body: field.text().notNull(),`;
46
+ expect(ensureNotesBodyEmbed(src)).toBe(`body: field.text().searchable().embed().notNull(),`);
47
+ });
48
+ });
49
+
50
+ describe("ensureAppStoreSearchEmbed", () => {
51
+ test("expands oke({ name }) with store.search.embed + embedModel import", () => {
52
+ const src = `import "@/core";
53
+ import { oke } from "okengine/http";
54
+
55
+ export const app = oke({ name: "notes" });
56
+ `;
57
+ const next = ensureAppStoreSearchEmbed(src, DEFAULT_SEARCH_EMBED_DIMS);
58
+ expect(next).toContain(`import { embedModel } from "@/core";`);
59
+ expect(next).toContain(`embed: { model: embedModel, dims: ${DEFAULT_SEARCH_EMBED_DIMS} }`);
60
+ expect(next).toContain(`name: "notes"`);
61
+ });
62
+
63
+ test("is idempotent when store.search.embed already set", () => {
64
+ const src = `import { embedModel } from "@/core";
65
+ export const app = oke({
66
+ name: "notes",
67
+ store: {
68
+ search: {
69
+ embed: { model: embedModel, dims: 768 },
70
+ },
71
+ },
72
+ });
73
+ `;
74
+ expect(ensureAppStoreSearchEmbed(src, 768)).toBe(src);
75
+ });
76
+ });
77
+
78
+ describe("renderAiTs embed", () => {
79
+ test("documents SQL .embed() vs index ai.embed pipeline", () => {
80
+ const out = renderAiTs({
81
+ driver: "openai-compatible",
82
+ provider: "openai-compatible",
83
+ embedModel: "nomic-embed-text",
84
+ });
85
+ expect(out).toContain("export const embedModel");
86
+ expect(out).toContain("store: { search: { embed } }");
87
+ expect(out).toContain("export const docsEmbed = ai.embed");
88
+ expect(out).toContain("separate from SQL");
89
+ });
90
+ });
91
+
92
+ describe("mergeAiIntoCore vs template comments", () => {
93
+ test("comment-only ai.model examples still get a full smart/local merge", () => {
94
+ const existing = `import { channel, gate, store, vault } from "okengine";
95
+
96
+ // --- AI ----------------------------------------------------------------------
97
+ // Registry cloud example:
98
+ // ai.model("smart", { provider: "openrouter", model: "openrouter/free" })
99
+ `;
100
+ const rendered = renderAiTs({
101
+ driver: "openai-compatible",
102
+ provider: "openrouter",
103
+ apiKeyEnv: "OPENROUTER_API_KEY",
104
+ chatModel: "openrouter/free",
105
+ });
106
+ // Template comments must not short-circuit to ensureSummarizeNotePrompt-only.
107
+ expect(ensureSummarizeNotePrompt(existing)).toBe(existing);
108
+ expect(isIncompleteAiSetup(existing)).toBe(false);
109
+
110
+ const merged = resolveAiCoreSource(existing, rendered);
111
+ expect(merged).toContain('import { ai, channel, gate, store, vault } from "okengine"');
112
+ expect(merged).toContain('export const smart = ai.model("smart"');
113
+ expect(merged).toContain('export const local = ai.model("local"');
114
+ expect(merged).toContain("export const summarizeNote");
115
+ });
116
+
117
+ test("repairs local+summarizeNote stubs missing smart and ai import", () => {
118
+ const broken = `import { channel, gate, store, vault } from "okengine";
119
+
120
+ // --- AI ----------------------------------------------------------------------
121
+ // ai.model("smart", { provider: "openrouter", model: "openrouter/free" })
122
+
123
+ /** Local OpenAI-compatible binding (via \`OKE_AI_URL\`). */
124
+ export const local = ai.model("local", {
125
+ provider: "openai-compatible",
126
+ model: process.env.OKE_AI_LOCAL_MODEL ?? "local-model",
127
+ ...(process.env.OKE_AI_URL?.trim() ? { baseUrl: process.env.OKE_AI_URL.trim() } : {}),
128
+ });
129
+
130
+ /** Advanced Notes summarize — used by \`notes.summarize\` via \`fx.ask\`. */
131
+ export const summarizeNote = smart.prompt("summarize-note", {
132
+ via: ["smart", "local"],
133
+ timeout: "30s",
134
+ });
135
+ `;
136
+ expect(isIncompleteAiSetup(broken)).toBe(true);
137
+ const rendered = renderAiTs({
138
+ driver: "openai-compatible",
139
+ provider: "openrouter",
140
+ apiKeyEnv: "OPENROUTER_API_KEY",
141
+ chatModel: "openrouter/free",
142
+ });
143
+ const fixed = resolveAiCoreSource(broken, rendered);
144
+ expect(fixed).toContain('import { ai, channel, gate, store, vault } from "okengine"');
145
+ expect(fixed).toContain('export const smart = ai.model("smart"');
146
+ expect(fixed).toContain('provider: "openrouter"');
147
+ expect(fixed.match(/export const local = ai\.model/g)?.length).toBe(1);
148
+ expect(fixed.match(/export const summarizeNote/g)?.length).toBe(1);
149
+ });
150
+ });
151
+
152
+ describe("AI API key vault + env persistence", () => {
153
+ test("envNameToCamelBinding", () => {
154
+ expect(envNameToCamelBinding("OPENROUTER_API_KEY")).toBe("openrouterApiKey");
155
+ expect(envNameToCamelBinding("ANTHROPIC_API_KEY")).toBe("anthropicApiKey");
156
+ });
157
+
158
+ test("upsertEnv uncomments an empty # KEY= placeholder", () => {
159
+ const env = `# OPENROUTER_API_KEY=\n# OKE_AI_MODEL=openrouter/free\n`;
160
+ const next = upsertEnv(env, "OPENROUTER_API_KEY", "sk-or-v1-test");
161
+ expect(next).toMatch(/^OPENROUTER_API_KEY=sk-or-v1-test$/m);
162
+ expect(next).not.toMatch(/^#\s*OPENROUTER_API_KEY=/m);
163
+ });
164
+
165
+ test("mergeAiApiKeyVaultSecret inserts under Vault heading", () => {
166
+ const existing = `import { channel, gate, store, vault } from "okengine";
167
+
168
+ // --- Vault -------------------------------------------------------------------
169
+
170
+ export const webhookSecret = vault.secret("APP_WEBHOOK_SECRET", {
171
+ description: "HMAC",
172
+ dev: "dev-webhook-secret-change-me",
173
+ });
174
+
175
+ // --- AI ----------------------------------------------------------------------
176
+ `;
177
+ const next = mergeAiApiKeyVaultSecret(existing, "OPENROUTER_API_KEY");
178
+ expect(hasAiApiKeyVaultSecret(next, "OPENROUTER_API_KEY")).toBe(true);
179
+ expect(next).toContain('export const openrouterApiKey = vault.secret("OPENROUTER_API_KEY"');
180
+ expect(next.indexOf("openrouterApiKey")).toBeLessThan(next.indexOf("webhookSecret"));
181
+ expect(mergeAiApiKeyVaultSecret(next, "OPENROUTER_API_KEY")).toBe(next);
182
+ });
183
+
184
+ test("mergeAiApiKeyVaultSecret prepends into NOTES_VAULT list", () => {
185
+ const existing = `import { vault } from "okengine";
186
+
187
+ export const webhookSecret = vault.secret("APP_WEBHOOK_SECRET", {
188
+ description: "HMAC",
189
+ dev: "x",
190
+ });
191
+
192
+ export const NOTES_VAULT = [
193
+ webhookSecret,
194
+ ] as const;
195
+ `;
196
+ const next = mergeAiApiKeyVaultSecret(existing, "OPENROUTER_API_KEY");
197
+ expect(next).toContain('export const openrouterApiKey = vault.secret("OPENROUTER_API_KEY"');
198
+ expect(next).toMatch(/NOTES_VAULT = \[\s*\n\s*openrouterApiKey,\s*\n\s*webhookSecret,/);
199
+ expect(mergeAiApiKeyVaultSecret(next, "OPENROUTER_API_KEY")).toBe(next);
200
+ });
201
+
202
+ test("ensureAiApiKeyVaultSecret prefers src/vault.ts over core.ts", () => {
203
+ const dir = mkdtempSync(join(tmpdir(), "create-oke-vault-ts-"));
204
+ try {
205
+ mkdirSync(join(dir, "src"), { recursive: true });
206
+ writeFileSync(
207
+ join(dir, "src", "vault.ts"),
208
+ `import { vault } from "okengine";
209
+ export const webhookSecret = vault.secret("APP_WEBHOOK_SECRET", { description: "HMAC", dev: "x" });
210
+ export const NOTES_VAULT = [
211
+ webhookSecret,
212
+ ] as const;
213
+ `,
214
+ "utf8",
215
+ );
216
+ writeFileSync(join(dir, "src", "core.ts"), `export * from "@/vault";\n`, "utf8");
217
+ ensureAiApiKeyVaultSecret(dir, "OPENROUTER_API_KEY");
218
+ const vault = readFileSync(join(dir, "src", "vault.ts"), "utf8");
219
+ const core = readFileSync(join(dir, "src", "core.ts"), "utf8");
220
+ expect(vault).toContain('vault.secret("OPENROUTER_API_KEY"');
221
+ expect(vault).toMatch(/NOTES_VAULT = \[\s*\n\s*openrouterApiKey,/);
222
+ expect(core).not.toContain("OPENROUTER_API_KEY");
223
+ } finally {
224
+ rmSync(dir, { recursive: true, force: true });
225
+ }
226
+ });
227
+
228
+ test("applyAiSetup writes apiKey into .env.local and vault.secret into core.ts", () => {
229
+ const dir = mkdtempSync(join(tmpdir(), "create-oke-ai-key-"));
230
+ try {
231
+ writeFileSync(
232
+ join(dir, "oke.config.ts"),
233
+ `import { defineConfig } from "okengine/config";
234
+ export default defineConfig({
235
+ drivers: {
236
+ channel: { email: { dev: "console", test: "console", prod: "console" } },
237
+ },
238
+ });
239
+ `,
240
+ "utf8",
241
+ );
242
+ mkdirSync(join(dir, "src"), { recursive: true });
243
+ writeFileSync(
244
+ join(dir, "src", "core.ts"),
245
+ `import { store, vault } from "okengine";
246
+
247
+ // --- Vault -------------------------------------------------------------------
248
+
249
+ export const webhookSecret = vault.secret("APP_WEBHOOK_SECRET", {
250
+ description: "HMAC",
251
+ dev: "dev-secret",
252
+ });
253
+
254
+ // --- AI ----------------------------------------------------------------------
255
+ `,
256
+ "utf8",
257
+ );
258
+ writeFileSync(join(dir, ".env.local"), `# OPENROUTER_API_KEY=\n`, "utf8");
259
+
260
+ applyAiSetup(dir, {
261
+ driver: "openai-compatible",
262
+ provider: "openrouter",
263
+ chatModel: "openrouter/free",
264
+ apiKeyEnv: "OPENROUTER_API_KEY",
265
+ apiKey: "sk-or-v1-from-wizard",
266
+ });
267
+
268
+ const env = readFileSync(join(dir, ".env.local"), "utf8");
269
+ expect(env).toMatch(/^OPENROUTER_API_KEY=sk-or-v1-from-wizard$/m);
270
+ const core = readFileSync(join(dir, "src", "core.ts"), "utf8");
271
+ expect(core).toContain('vault.secret("OPENROUTER_API_KEY"');
272
+ expect(core).toContain('export const smart = ai.model("smart"');
273
+ expect(existsSync(join(dir, "src", "core", "ai.ts"))).toBe(false);
274
+ } finally {
275
+ rmSync(dir, { recursive: true, force: true });
276
+ }
277
+ });
278
+ });