create-oke 0.2.3 → 0.2.8

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 (71) hide show
  1. package/README.md +32 -20
  2. package/examples/linkly/.env.example +15 -0
  3. package/examples/linkly/drizzle.config.ts +11 -0
  4. package/examples/linkly/oke.config.ts +6 -4
  5. package/examples/linkly/package.json +7 -4
  6. package/examples/linkly/src/flows/analytics/index.ts +29 -14
  7. package/examples/linkly/src/flows/links/index.ts +58 -35
  8. package/examples/linkly/src/flows/links/signals.ts +4 -3
  9. package/examples/linkly/src/gates.ts +4 -2
  10. package/examples/linkly/src/schema.decl.ts +39 -0
  11. package/examples/linkly/src/schema.generated.ts +30 -0
  12. package/examples/linkly/src/schema.ts +2 -17
  13. package/examples/linkly/tests/linkly.test.ts +9 -9
  14. package/examples/notes/.env.example +12 -0
  15. package/examples/notes/drizzle.config.ts +11 -0
  16. package/examples/notes/oke.config.ts +8 -1
  17. package/examples/notes/package.json +7 -4
  18. package/examples/notes/src/app.ts +1 -1
  19. package/examples/notes/src/flows/notes/index.ts +44 -30
  20. package/examples/notes/src/schema.ts +3 -3
  21. package/examples/notes/tests/notes.test.ts +1 -1
  22. package/examples/provisions/.env.example +19 -0
  23. package/examples/provisions/drizzle.config.ts +11 -0
  24. package/examples/provisions/oke.config.ts +10 -8
  25. package/examples/provisions/package.json +7 -4
  26. package/examples/provisions/src/app.ts +3 -3
  27. package/examples/provisions/src/channels.ts +4 -3
  28. package/examples/provisions/src/flows/notifications/index.ts +13 -7
  29. package/examples/provisions/src/flows/orders/index.ts +58 -31
  30. package/examples/provisions/src/flows/payments/index.ts +12 -6
  31. package/examples/provisions/src/schema.ts +7 -7
  32. package/examples/provisions/src/vault.ts +2 -2
  33. package/examples/provisions/tests/orders.test.ts +9 -8
  34. package/examples/skyport/.env.example +34 -0
  35. package/examples/skyport/drizzle.config.ts +11 -0
  36. package/examples/skyport/oke.config.ts +25 -22
  37. package/examples/skyport/package.json +8 -5
  38. package/examples/skyport/src/ai.ts +4 -4
  39. package/examples/skyport/src/app.ts +2 -13
  40. package/examples/skyport/src/flows/bookings/index.ts +45 -22
  41. package/examples/skyport/src/flows/bookings/shapes.ts +4 -4
  42. package/examples/skyport/src/flows/bookings/signals.ts +6 -2
  43. package/examples/skyport/src/flows/support/index.ts +37 -25
  44. package/examples/skyport/src/schema.ts +11 -8
  45. package/examples/skyport/src/vault.ts +1 -1
  46. package/examples/skyport/tests/support.test.ts +6 -6
  47. package/package.json +9 -9
  48. package/src/cli.test.ts +132 -64
  49. package/src/cli.ts +58 -33
  50. package/src/index.ts +0 -0
  51. package/src/scaffold.ts +53 -15
  52. package/src/sync-templates.ts +2 -11
  53. package/src/templates.ts +1 -2
  54. package/src/transform.ts +59 -9
  55. package/templates/full/.env.example +24 -0
  56. package/templates/full/README.md +22 -6
  57. package/templates/full/drizzle.config.ts +14 -0
  58. package/templates/full/oke.config.ts +38 -9
  59. package/templates/full/package.json +3 -0
  60. package/templates/hello/.env.example +9 -0
  61. package/templates/hello/README.md +6 -4
  62. package/templates/minimal/.env.example +13 -0
  63. package/templates/minimal/README.md +15 -7
  64. package/templates/minimal/drizzle.config.ts +14 -0
  65. package/templates/minimal/oke.config.ts +8 -1
  66. package/templates/minimal/package.json +3 -0
  67. package/templates/standard/.env.example +21 -0
  68. package/templates/standard/README.md +25 -8
  69. package/templates/standard/drizzle.config.ts +14 -0
  70. package/templates/standard/oke.config.ts +36 -8
  71. package/templates/standard/package.json +3 -0
@@ -2,8 +2,12 @@ import { signal } from "okengine";
2
2
  import { z } from "zod";
3
3
 
4
4
  export const orderPlaced = signal("order-placed", {
5
- schema: z.object({ orderId: z.string() }), delivery: "once", retries: 5, deadLetter: true,
5
+ schema: z.object({ orderId: z.string() }),
6
+ delivery: "once",
7
+ retries: 5,
8
+ deadLetter: true,
6
9
  });
7
10
  export const seatFeed = signal("seat-feed", {
8
- schema: z.object({ flightId: z.string(), left: z.number() }), delivery: "live",
11
+ schema: z.object({ flightId: z.string(), left: z.number() }),
12
+ delivery: "live",
9
13
  });
@@ -6,33 +6,45 @@ import { db } from "../../core";
6
6
  import { tickets } from "../../schema";
7
7
 
8
8
  // ① A prompt call with a provider fallback chain and a validated result
9
- export const createTicket = on(http.post("/tickets").gate(member), flow({
10
- in: z.object({ subject: z.string(), body: z.string() }),
11
- out: z.object({ id: z.string(), urgency: z.string() }),
12
- do: async (input, fx) => {
13
- const t = await fx.ask(triage, input, { via: [smart, fast] });
14
- const id = fx.id();
15
- await fx.store(db).insert(tickets).values({ id, ...input, ...t });
16
- return { id, urgency: t.urgency };
17
- },
18
- }));
9
+ export const createTicket = on(
10
+ http.post("/tickets").gate(member),
11
+ flow({
12
+ in: z.object({ subject: z.string(), body: z.string() }),
13
+ out: z.object({ id: z.string(), urgency: z.string() }),
14
+ do: async (input, fx) => {
15
+ const t = await fx.ask(triage, input, { via: [smart, fast] });
16
+ const id = fx.id();
17
+ await fx
18
+ .store(db)
19
+ .insert(tickets)
20
+ .values({ id, ...input, ...t });
21
+ return { id, urgency: t.urgency };
22
+ },
23
+ }),
24
+ );
19
25
  // effects → writes[sql:tickets] asks[ticket-triage v3] cost[~$0.01] nondeterministic
20
26
 
21
27
  // ② RAG — retrieve, then answer with streaming tokens
22
- export const askDocs = on(http.post("/ask").gate(member).live(), flow({
23
- in: z.object({ question: z.string() }),
24
- do: async ({ question }, fx) => {
25
- const context = await fx.search(embed, question, { topK: 5 });
26
- return fx.stream(smart, { prompt: "answer-with-context", data: { question, context } });
27
- // streaming reaches the client through the Signal element — no separate socket layer
28
- },
29
- }));
28
+ export const askDocs = on(
29
+ http.post("/ask").gate(member).live(),
30
+ flow({
31
+ in: z.object({ question: z.string() }),
32
+ do: async ({ question }, fx) => {
33
+ const context = await fx.search(embed, question, { topK: 5 });
34
+ return fx.stream(smart, { prompt: "answer-with-context", data: { question, context } });
35
+ // streaming reaches the client through the Signal element — no separate socket layer
36
+ },
37
+ }),
38
+ );
30
39
 
31
40
  // ③ A durable, bounded agent
32
- export const supportAgent = on(http.post("/support").gate(member), flow({
33
- durable: true, // nondeterministic calls are ALWAYS journaled
34
- in: z.object({ message: z.string() }),
35
- do: ({ message }, fx) => fx.run(support, { message }),
36
- // the agent can only call getBooking and refundBooking, and only within THIS user's
37
- // gates and tenant scope it cannot exceed what the code declares
38
- }));
41
+ export const supportAgent = on(
42
+ http.post("/support").gate(member),
43
+ flow({
44
+ durable: true, // nondeterministic calls are ALWAYS journaled
45
+ in: z.object({ message: z.string() }),
46
+ do: ({ message }, fx) => fx.run(support, { message }),
47
+ // the agent can only call getBooking and refundBooking, and only within THIS user's
48
+ // gates and tenant scope — it cannot exceed what the code declares
49
+ }),
50
+ );
@@ -2,21 +2,24 @@ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
2
2
  import { id } from "okengine/store";
3
3
 
4
4
  export const bookings = sqliteTable("bookings", {
5
- id: text("id").primaryKey().$defaultFn(id),
6
- userId: text("user_id").notNull(),
7
- flightId: text("flight_id").notNull(),
8
- seats: integer("seats").notNull(),
9
- status: text("status").notNull().default("pending"),
5
+ id: text("id").primaryKey().$defaultFn(id),
6
+ userId: text("user_id").notNull(),
7
+ flightId: text("flight_id").notNull(),
8
+ seats: integer("seats").notNull(),
9
+ status: text("status").notNull().default("pending"),
10
10
  createdAt: integer("created_at").notNull(),
11
11
  });
12
12
 
13
13
  export const flights = sqliteTable("flights", {
14
- id: text("id").primaryKey(),
14
+ id: text("id").primaryKey(),
15
15
  seatsAvailable: integer("seats_available").notNull(),
16
16
  });
17
17
 
18
18
  export const tickets = sqliteTable("tickets", {
19
- id: text("id").primaryKey(), subject: text("subject").notNull(),
20
- body: text("body").notNull(), urgency: text("urgency"), team: text("team"),
19
+ id: text("id").primaryKey(),
20
+ subject: text("subject").notNull(),
21
+ body: text("body").notNull(),
22
+ urgency: text("urgency"),
23
+ team: text("team"),
21
24
  summary: text("summary"),
22
25
  });
@@ -3,7 +3,7 @@ import { z } from "zod";
3
3
 
4
4
  export const dbUrl = vault.secret("DATABASE_URL", {
5
5
  schema: z.string().url(),
6
- dev: vault.fromStack("store.sql"),
6
+ dev: vault.fromDocker("store.sql"),
7
7
  });
8
8
 
9
9
  export const dbReplica1 = vault.secret("DATABASE_REPLICA_1", {
@@ -4,13 +4,13 @@ import { app } from "../src/app";
4
4
  import { triage } from "../src/ai";
5
5
 
6
6
  test("AI triage is mockable and cost-bounded", async () => {
7
- const t = await createTestApp(app);
8
- t.ai.mock(triage, { urgency: "high", team: "ops", summary: "seat dispute" });
7
+ const t = await createTestApp(app);
8
+ t.ai.mock(triage, { urgency: "high", team: "ops", summary: "seat dispute" });
9
9
 
10
- const u = await t.auth.loginAs({});
11
- const { data } = await t.api.support.createTicket({ subject: "…", body: "…" }, { as: u });
10
+ const u = await t.auth.loginAs({});
11
+ const { data } = await t.api.support.createTicket({ subject: "…", body: "…" }, { as: u });
12
12
 
13
- expect(data!.urgency).toBe("high");
14
- expect(t.ai.cost()).toBeLessThan(0.02); // budgets are assertable
13
+ expect(data!.urgency).toBe("high");
14
+ expect(t.ai.cost()).toBeLessThan(0.02); // budgets are assertable
15
15
  await t.close();
16
16
  });
package/package.json CHANGED
@@ -1,10 +1,8 @@
1
1
  {
2
2
  "name": "create-oke",
3
- "version": "0.2.3",
3
+ "version": "0.2.8",
4
4
  "description": "Scaffold an okengine app — bunx create-oke@latest <name>",
5
5
  "license": "MIT",
6
- "type": "module",
7
- "sideEffects": false,
8
6
  "repository": {
9
7
  "type": "git",
10
8
  "url": "git+https://github.com/omqkhafi/okengine.git",
@@ -13,28 +11,30 @@
13
11
  "bin": {
14
12
  "create-oke": "src/index.ts"
15
13
  },
16
- "exports": {
17
- ".": "./src/index.ts"
18
- },
19
14
  "files": [
20
15
  "src",
21
16
  "templates",
22
17
  "examples"
23
18
  ],
19
+ "type": "module",
20
+ "sideEffects": false,
21
+ "exports": {
22
+ ".": "./src/index.ts"
23
+ },
24
24
  "scripts": {
25
25
  "prepack": "bun ./src/sync-templates.ts",
26
26
  "typecheck": "tsc --noEmit",
27
27
  "test": "bun test",
28
28
  "test:integration": "CREATE_OKE_INTEGRATION=1 bun test tests/scaffold.integration.test.ts tests/dev.integration.test.ts"
29
29
  },
30
- "engines": {
31
- "bun": ">=1.3"
32
- },
33
30
  "dependencies": {
34
31
  "@clack/prompts": "^1.7.0"
35
32
  },
36
33
  "devDependencies": {
37
34
  "@types/bun": "latest",
38
35
  "typescript": "^7.0.2"
36
+ },
37
+ "engines": {
38
+ "bun": ">=1.3"
39
39
  }
40
40
  }
package/src/cli.test.ts CHANGED
@@ -3,13 +3,7 @@
3
3
  */
4
4
 
5
5
  import { describe, expect, test } from "bun:test";
6
- import {
7
- existsSync,
8
- mkdtempSync,
9
- readFileSync,
10
- readdirSync,
11
- rmSync,
12
- } from "node:fs";
6
+ import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
13
7
  import { tmpdir } from "node:os";
14
8
  import { join } from "node:path";
15
9
  import {
@@ -36,7 +30,9 @@ import {
36
30
  import {
37
31
  sanitizeProjectName,
38
32
  shouldSkipTemplatePath,
33
+ transformConfigForSqlDriver,
39
34
  transformPackageJson,
35
+ transformSchemaForSqlDriver,
40
36
  } from "./transform.ts";
41
37
 
42
38
  describe("parseArgs", () => {
@@ -56,20 +52,25 @@ describe("parseArgs", () => {
56
52
  });
57
53
 
58
54
  test("accepts --from-example", () => {
59
- expect(parseArgs(["x", "--from-example", "notes"]).fromExample).toBe(
60
- "notes",
61
- );
62
- expect(parseArgs(["x", "--from-example=skyport"]).fromExample).toBe(
63
- "skyport",
64
- );
55
+ expect(parseArgs(["x", "--from-example", "notes"]).fromExample).toBe("notes");
56
+ expect(parseArgs(["x", "--from-example=skyport"]).fromExample).toBe("skyport");
57
+ });
58
+
59
+ test("accepts --sql sqlite|postgres", () => {
60
+ expect(parseArgs(["x"]).sqlDriver).toBe("sqlite");
61
+ expect(parseArgs(["x"]).sqlDriverExplicit).toBe(false);
62
+ expect(parseArgs(["x", "--sql", "postgres"]).sqlDriver).toBe("postgres");
63
+ expect(parseArgs(["x", "--sql=sqlite"]).sqlDriver).toBe("sqlite");
64
+ expect(parseArgs(["x", "--sql", "postgres"]).sqlDriverExplicit).toBe(true);
65
65
  });
66
66
 
67
- test("rejects unknown template / example / both", () => {
67
+ test("rejects unknown template / example / sql / both", () => {
68
68
  expect(() => parseArgs(["x", "--template", "nope"])).toThrow(/template/);
69
69
  expect(() => parseArgs(["x", "--from-example", "nope"])).toThrow(/example/);
70
- expect(() =>
71
- parseArgs(["x", "--template", "hello", "--from-example", "notes"]),
72
- ).toThrow(/either/);
70
+ expect(() => parseArgs(["x", "--sql", "mysql"])).toThrow(/sql/);
71
+ expect(() => parseArgs(["x", "--template", "hello", "--from-example", "notes"])).toThrow(
72
+ /either/,
73
+ );
73
74
  });
74
75
  });
75
76
 
@@ -86,13 +87,9 @@ describe("shouldPrompt", () => {
86
87
  expect(shouldPrompt(parseArgs([]), false)).toBe(false);
87
88
  expect(shouldPrompt(parseArgs(["my-app"]), false)).toBe(false);
88
89
  expect(shouldPrompt(parseArgs(["my-app", "--yes"]), true)).toBe(false);
89
- expect(shouldPrompt(parseArgs(["my-app", "--template", "hello"]), true)).toBe(
90
- false,
91
- );
90
+ expect(shouldPrompt(parseArgs(["my-app", "--template", "hello"]), true)).toBe(false);
92
91
  expect(shouldPrompt(parseArgs(["--template", "hello"]), true)).toBe(false);
93
- expect(shouldPrompt(parseArgs(["--from-example", "notes"]), true)).toBe(
94
- false,
95
- );
92
+ expect(shouldPrompt(parseArgs(["--from-example", "notes"]), true)).toBe(false);
96
93
  expect(shouldPrompt(parseArgs(["--help"]), true)).toBe(false);
97
94
  });
98
95
  });
@@ -108,17 +105,16 @@ describe("parseArgs flags", () => {
108
105
  });
109
106
 
110
107
  test("rejects --install with --no-install", () => {
111
- expect(() => parseArgs(["x", "--install", "--no-install"])).toThrow(
112
- /install/,
113
- );
108
+ expect(() => parseArgs(["x", "--install", "--no-install"])).toThrow(/install/);
114
109
  });
115
110
  });
116
111
 
117
112
  describe("sourceFromArgs", () => {
118
113
  test("prefers example when set", () => {
119
- expect(sourceFromArgs(parseArgs(["x", "--from-example", "linkly"]))).toEqual(
120
- { kind: "example", id: "linkly" },
121
- );
114
+ expect(sourceFromArgs(parseArgs(["x", "--from-example", "linkly"]))).toEqual({
115
+ kind: "example",
116
+ id: "linkly",
117
+ });
122
118
  expect(sourceFromArgs(parseArgs(["x"]))).toEqual({
123
119
  kind: "template",
124
120
  id: DEFAULT_TEMPLATE,
@@ -136,11 +132,10 @@ describe("scaffoldArgsFromAnswers ≡ flag-driven", () => {
136
132
  agentsMd: true,
137
133
  };
138
134
  const fromAnswers = scaffoldArgsFromAnswers(answers);
139
- const fromFlags = scaffoldArgsFromCli(
140
- parseArgs(["x", "--template", id]),
141
- );
135
+ const fromFlags = scaffoldArgsFromCli(parseArgs(["x", "--template", id]));
142
136
  expect(fromAnswers).toEqual(fromFlags);
143
137
  expect(fromAnswers.source).toEqual({ kind: "template", id });
138
+ expect(fromAnswers.sqlDriver).toBe("sqlite");
144
139
  }
145
140
  });
146
141
 
@@ -154,14 +149,27 @@ describe("scaffoldArgsFromAnswers ≡ flag-driven", () => {
154
149
  agentsMd: true,
155
150
  };
156
151
  const fromAnswers = scaffoldArgsFromAnswers(answers);
157
- const fromFlags = scaffoldArgsFromCli(
158
- parseArgs(["x", "--from-example", id]),
159
- );
152
+ const fromFlags = scaffoldArgsFromCli(parseArgs(["x", "--from-example", id]));
160
153
  expect(fromAnswers).toEqual(fromFlags);
161
154
  expect(fromAnswers.source).toEqual({ kind: "example", id });
155
+ expect(fromAnswers.sqlDriver).toBe("sqlite");
162
156
  }
163
157
  });
164
158
 
159
+ test("interactive never opts into --sql postgres (flag-only)", () => {
160
+ const answers: InteractiveAnswers = {
161
+ name: "x",
162
+ choice: "standard",
163
+ installAndRun: false,
164
+ agentsMd: true,
165
+ };
166
+ expect(scaffoldArgsFromAnswers(answers).sqlDriver).toBe("sqlite");
167
+ expect(
168
+ scaffoldArgsFromCli(parseArgs(["x", "--template", "standard", "--sql", "postgres"]))
169
+ .sqlDriver,
170
+ ).toBe("postgres");
171
+ });
172
+
165
173
  test("8 paths cover every template and every example", () => {
166
174
  expect(TEMPLATES.length + EXAMPLES.length).toBe(8);
167
175
  });
@@ -189,11 +197,51 @@ describe("transformPackageJson", () => {
189
197
  });
190
198
  });
191
199
 
200
+ describe("transformSchemaForSqlDriver", () => {
201
+ const sqliteSchema = `import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
202
+ export const pings = sqliteTable("pings", {});
203
+ `;
204
+
205
+ test("postgres swaps dialect imports and table helper", () => {
206
+ const next = transformSchemaForSqlDriver(sqliteSchema, "postgres");
207
+ expect(next).toContain('from "drizzle-orm/pg-core"');
208
+ expect(next).toContain("pgTable(");
209
+ expect(next).not.toContain("sqliteTable");
210
+ expect(next).not.toContain("sqlite-core");
211
+ });
212
+
213
+ test("sqlite is idempotent on template sources", () => {
214
+ expect(transformSchemaForSqlDriver(sqliteSchema, "sqlite")).toBe(sqliteSchema);
215
+ });
216
+ });
217
+
218
+ describe("transformConfigForSqlDriver", () => {
219
+ const config = `export default defineConfig({
220
+ drivers: {
221
+ store: {
222
+ sql: {
223
+ local: "sqlite",
224
+ docker: "postgres",
225
+ test: "memory",
226
+ prod: "postgres",
227
+ },
228
+ },
229
+ },
230
+ });
231
+ `;
232
+
233
+ test("postgres pins local/docker/prod and leaves test memory", () => {
234
+ const next = transformConfigForSqlDriver(config, "postgres");
235
+ expect(next).toContain('local: "postgres"');
236
+ expect(next).toContain('docker: "postgres"');
237
+ expect(next).toContain('prod: "postgres"');
238
+ expect(next).toContain('test: "memory"');
239
+ });
240
+ });
241
+
192
242
  describe("shouldSkipTemplatePath", () => {
193
243
  test("skips node_modules, locks, and monorepo docker test", () => {
194
- expect(shouldSkipTemplatePath("node_modules/okengine/package.json")).toBe(
195
- true,
196
- );
244
+ expect(shouldSkipTemplatePath("node_modules/okengine/package.json")).toBe(true);
197
245
  expect(shouldSkipTemplatePath("bun.lock")).toBe(true);
198
246
  expect(shouldSkipTemplatePath("tests/docker.test.ts")).toBe(true);
199
247
  expect(shouldSkipTemplatePath("tests/support.test.ts")).toBe(false);
@@ -225,23 +273,23 @@ describe("scaffold structure", () => {
225
273
  name: `app-${id}`,
226
274
  source: { kind: "template", id },
227
275
  });
228
- expect([...result.files].sort()).toEqual(
229
- [...expected, "AGENTS.md"].sort(),
230
- );
276
+ const extras = ["AGENTS.md"];
277
+ if (expected.includes(".env.example")) extras.push(".env.local");
278
+ expect([...result.files].sort()).toEqual([...expected, ...extras].sort());
231
279
  expect(result.files).toContain(".gitignore");
232
280
  expect(result.files).toContain("README.md");
233
- expect(
234
- readFileSync(join(result.targetDir, "AGENTS.md"), "utf8"),
235
- ).toMatch(/one law|on\(Trigger\)/i);
281
+ expect(result.sqlDriver).toBe("sqlite");
282
+ expect(readFileSync(join(result.targetDir, "AGENTS.md"), "utf8")).toMatch(
283
+ /one law|on\(Trigger\)/i,
284
+ );
236
285
  const readme = readFileSync(join(result.targetDir, "README.md"), "utf8");
237
286
  expect(readme).toMatch(/oke dev/);
238
287
  expect(readme).toMatch(new RegExp(`^# ${id}`, "m"));
239
- expect(
240
- readFileSync(join(result.targetDir, ".gitignore"), "utf8"),
241
- ).toMatch(/node_modules/);
242
- const pkg = JSON.parse(
243
- readFileSync(join(result.targetDir, "package.json"), "utf8"),
244
- ) as { name: string; dependencies: { okengine: string } };
288
+ expect(readFileSync(join(result.targetDir, ".gitignore"), "utf8")).toMatch(/node_modules/);
289
+ const pkg = JSON.parse(readFileSync(join(result.targetDir, "package.json"), "utf8")) as {
290
+ name: string;
291
+ dependencies: { okengine: string };
292
+ };
245
293
  expect(pkg.name).toBe(`app-${id}`);
246
294
  expect(pkg.dependencies.okengine).not.toBe("file:../..");
247
295
  } finally {
@@ -258,14 +306,9 @@ describe("scaffold structure", () => {
258
306
  name: "hello-app",
259
307
  source: { kind: "template", id: "hello" },
260
308
  });
261
- const flowFiles = result.files.filter(
262
- (f) => f.startsWith("src/flows/") && f.endsWith(".ts"),
263
- );
309
+ const flowFiles = result.files.filter((f) => f.startsWith("src/flows/") && f.endsWith(".ts"));
264
310
  expect(flowFiles).toEqual(["src/flows/hello/index.ts"]);
265
- const flowSrc = readFileSync(
266
- join(result.targetDir, "src/flows/hello/index.ts"),
267
- "utf8",
268
- );
311
+ const flowSrc = readFileSync(join(result.targetDir, "src/flows/hello/index.ts"), "utf8");
269
312
  expect(flowSrc).toMatch(/export const hello/);
270
313
  expect(flowSrc).not.toMatch(/\bstore\b/);
271
314
  expect(result.files.some((f) => f.includes("core.ts"))).toBe(false);
@@ -323,15 +366,39 @@ describe("scaffold structure", () => {
323
366
  name: "from-notes",
324
367
  source: { kind: "example", id: "notes" },
325
368
  });
326
- expect([...result.files].sort()).toEqual(
327
- [...expected, "AGENTS.md"].sort(),
328
- );
369
+ const extras = ["AGENTS.md"];
370
+ if (expected.includes(".env.example")) extras.push(".env.local");
371
+ expect([...result.files].sort()).toEqual([...expected, ...extras].sort());
329
372
  expect(result.files).toContain("src/flows/notes/index.ts");
330
373
  } finally {
331
374
  rmSync(dir, { recursive: true, force: true });
332
375
  }
333
376
  });
334
377
 
378
+ test("--sql postgres rewrites schema dialect and pins store.sql", () => {
379
+ const dir = mkdtempSync(join(tmpdir(), "create-oke-sql-pg-"));
380
+ try {
381
+ const result = scaffold({
382
+ targetDir: join(dir, "pg-app"),
383
+ name: "pg-app",
384
+ source: { kind: "template", id: "standard" },
385
+ sqlDriver: "postgres",
386
+ });
387
+ expect(result.sqlDriver).toBe("postgres");
388
+ const schema = readFileSync(join(result.targetDir, "src/schema.ts"), "utf8");
389
+ expect(schema).toContain('from "drizzle-orm/pg-core"');
390
+ expect(schema).toContain("pgTable(");
391
+ expect(schema).not.toContain("sqliteTable");
392
+ const config = readFileSync(join(result.targetDir, "oke.config.ts"), "utf8");
393
+ expect(config).toMatch(/sql:\s*\{[\s\S]*local:\s*"postgres"/);
394
+ expect(config).toMatch(/sql:\s*\{[\s\S]*docker:\s*"postgres"/);
395
+ expect(config).toMatch(/sql:\s*\{[\s\S]*prod:\s*"postgres"/);
396
+ expect(config).toMatch(/sql:\s*\{[\s\S]*test:\s*"memory"/);
397
+ } finally {
398
+ rmSync(dir, { recursive: true, force: true });
399
+ }
400
+ });
401
+
335
402
  test("--no-agents-md skips AGENTS.md", () => {
336
403
  const dir = mkdtempSync(join(tmpdir(), "create-oke-no-agents-"));
337
404
  try {
@@ -376,10 +443,10 @@ describe("non-TTY CLI", () => {
376
443
  const root = mkdtempSync(join(tmpdir(), "create-oke-flag-"));
377
444
  const target = join(root, "flag-app");
378
445
  try {
379
- const code = await run(
380
- [target, "--template", "hello", "--no-install"],
381
- { stdinIsTTY: false, runPostScaffold: false },
382
- );
446
+ const code = await run([target, "--template", "hello", "--no-install"], {
447
+ stdinIsTTY: false,
448
+ runPostScaffold: false,
449
+ });
383
450
  expect(code).toBe(0);
384
451
  expect(readdirSync(target).length).toBeGreaterThan(0);
385
452
  expect(existsSync(join(target, "AGENTS.md"))).toBe(true);
@@ -390,6 +457,7 @@ describe("non-TTY CLI", () => {
390
457
  label: "hello",
391
458
  okengineDependency: "x",
392
459
  files: [],
460
+ sqlDriver: "sqlite" as const,
393
461
  };
394
462
  expect(nextStepsText(result)).toContain("oke dev");
395
463
  expect(nextStepsText(result)).toContain("bun install");