create-oke 0.18.4 → 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.
- package/README.md +1 -1
- package/package.json +3 -3
- package/src/agents-md.ts +6 -4
- package/src/ai-setup/apply.test.ts +278 -0
- package/src/ai-setup/apply.ts +430 -52
- package/src/ai-setup/catalog.ts +250 -1343
- package/src/ai-setup/from-pref.ts +3 -71
- package/src/ai-setup/prompts.ts +60 -443
- package/src/cli.test.ts +21 -15
- package/src/cli.ts +20 -11
- package/src/create-defaults.test.ts +4 -4
- package/src/create-defaults.ts +3 -3
- package/src/customize-flow.test.ts +9 -15
- package/src/customize-flow.ts +59 -66
- package/src/drivers-catalog.ts +18 -23
- package/src/transform.test.ts +69 -10
- package/src/transform.ts +10 -35
- package/templates/advanced/.env.example +28 -11
- package/templates/advanced/.github/workflows/ci.yml +1 -1
- package/templates/advanced/oke.config.ts +3 -4
- package/templates/advanced/package.json +11 -10
- package/templates/advanced/src/app.ts +36 -1
- package/templates/advanced/src/core.ts +12 -11
- package/templates/advanced/src/db/schema.decl.ts +6 -2
- package/templates/advanced/src/db/seed/index.ts +4 -4
- package/templates/advanced/src/flows/main/route.ts +3 -2
- package/templates/advanced/src/flows/notes/[id]/archive.ts +5 -8
- package/templates/advanced/src/flows/notes/[id]/attach.ts +1 -4
- package/templates/advanced/src/flows/notes/[id]/get.ts +4 -7
- package/templates/advanced/src/flows/notes/[id]/summarize.ts +3 -4
- package/templates/advanced/src/flows/notes/create.ts +4 -6
- package/templates/advanced/src/flows/notes/digest.ts +6 -5
- package/templates/advanced/src/flows/notes/list.ts +4 -5
- package/templates/advanced/src/flows/notes/shapes.ts +13 -3
- package/templates/advanced/src/flows/notes/signals.ts +1 -2
- package/templates/advanced/src/vault.ts +138 -0
- package/templates/advanced/tests/advanced.test.ts +11 -8
- package/templates/advanced/web/src/App.css +7 -0
- package/templates/advanced/web/src/App.tsx +101 -1
- package/templates/advanced/web/src/client.ts +16 -3
- package/templates/advanced/web/vite.config.ts +1 -0
- package/templates/standard/.env.example +22 -11
- package/templates/standard/.github/workflows/ci.yml +1 -1
- package/templates/standard/oke.config.ts +3 -3
- package/templates/standard/package.json +11 -10
- package/templates/standard/src/app.ts +6 -1
- package/templates/standard/src/core.ts +10 -11
- package/templates/standard/src/db/schema.decl.ts +6 -2
- package/templates/standard/src/db/seed/index.ts +3 -3
- package/templates/standard/src/flows/main/route.ts +3 -2
- package/templates/standard/src/flows/notes/[id]/archive.ts +5 -8
- package/templates/standard/src/flows/notes/[id]/get.ts +4 -7
- package/templates/standard/src/flows/notes/create.ts +4 -6
- package/templates/standard/src/flows/notes/list.ts +4 -5
- package/templates/standard/src/flows/notes/shapes.ts +12 -2
- package/templates/standard/src/flows/notes/signals.ts +1 -2
- package/templates/standard/src/vault.ts +123 -0
- package/templates/standard/tests/standard.test.ts +3 -1
- package/templates/standard/web/src/client.ts +2 -2
- package/templates/standard/web/vite.config.ts +1 -0
- package/src/ai-setup/detect-ollama.ts +0 -228
- 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.
|
|
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.
|
|
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.
|
|
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.
|
|
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(
|
|
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
|
-
-
|
|
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
|
+
});
|