create-oke 0.10.2 → 0.11.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 (59) hide show
  1. package/README.md +2 -2
  2. package/package.json +2 -2
  3. package/src/ai-setup/apply.ts +123 -31
  4. package/src/ai-setup/catalog.ts +3 -3
  5. package/src/ai-setup/from-pref.ts +1 -1
  6. package/src/ai-setup/prompts.ts +1 -1
  7. package/src/cli.test.ts +183 -142
  8. package/src/cli.ts +190 -32
  9. package/src/create-defaults.test.ts +21 -15
  10. package/src/create-defaults.ts +33 -13
  11. package/src/customize-flow.test.ts +30 -35
  12. package/src/customize-flow.ts +68 -229
  13. package/src/drivers-catalog.ts +42 -48
  14. package/src/local-okengine.test.ts +50 -41
  15. package/src/local-okengine.ts +28 -14
  16. package/src/locales.test.ts +128 -0
  17. package/src/locales.ts +321 -0
  18. package/src/scaffold.ts +277 -33
  19. package/src/templates.ts +2 -8
  20. package/src/transform.test.ts +102 -38
  21. package/src/transform.ts +378 -132
  22. package/templates/advanced/.env.example +23 -3
  23. package/templates/advanced/README.md +1 -1
  24. package/templates/advanced/drizzle.config.ts +10 -13
  25. package/templates/advanced/oke.config.ts +21 -62
  26. package/templates/advanced/package.json +3 -1
  27. package/templates/advanced/src/app.ts +3 -10
  28. package/templates/advanced/src/core.ts +58 -0
  29. package/templates/advanced/src/db/schema.decl.ts +1 -1
  30. package/templates/advanced/src/db/seed/index.ts +2 -2
  31. package/templates/advanced/src/flows/notes/index.ts +3 -15
  32. package/templates/advanced/src/locales/index.ts +6 -0
  33. package/templates/advanced/tests/advanced.test.ts +1 -1
  34. package/templates/advanced/tsconfig.json +3 -0
  35. package/templates/standard/.env.example +23 -3
  36. package/templates/standard/README.md +1 -1
  37. package/templates/standard/drizzle.config.ts +10 -13
  38. package/templates/standard/oke.config.ts +18 -60
  39. package/templates/standard/package.json +3 -1
  40. package/templates/standard/src/app.ts +3 -10
  41. package/templates/standard/src/core.ts +55 -0
  42. package/templates/standard/src/db/schema.decl.ts +1 -1
  43. package/templates/standard/src/db/seed/index.ts +2 -2
  44. package/templates/standard/src/flows/notes/index.ts +3 -12
  45. package/templates/standard/src/locales/index.ts +6 -0
  46. package/templates/standard/tests/standard.test.ts +1 -1
  47. package/templates/standard/tsconfig.json +3 -0
  48. package/templates/advanced/src/core/channels.ts +0 -13
  49. package/templates/advanced/src/core/gates.ts +0 -12
  50. package/templates/advanced/src/core/index.ts +0 -12
  51. package/templates/advanced/src/core/store.ts +0 -8
  52. package/templates/advanced/src/core/vault.ts +0 -7
  53. package/templates/advanced/src/locales/ar.ts +0 -18
  54. package/templates/standard/src/core/channels.ts +0 -13
  55. package/templates/standard/src/core/gates.ts +0 -12
  56. package/templates/standard/src/core/index.ts +0 -12
  57. package/templates/standard/src/core/store.ts +0 -5
  58. package/templates/standard/src/core/vault.ts +0 -7
  59. package/templates/standard/src/locales/ar.ts +0 -18
package/src/cli.test.ts CHANGED
@@ -35,22 +35,15 @@ import {
35
35
  toCreateDefaults,
36
36
  writeCreateDefaults,
37
37
  } from "./create-defaults.ts";
38
- import { pinsDockerReady, pinsLocalOnly, recommendedDefaults } from "./drivers-catalog.ts";
38
+ import { pinsDockerReady, recommendedDefaults } from "./drivers-catalog.ts";
39
39
  import { listTemplateFiles, scaffold, targetDirectoryBlockReason } from "./scaffold.ts";
40
- import {
41
- DEFAULT_TEMPLATE,
42
- TEMPLATE_DEFAULT_MODE,
43
- TEMPLATES,
44
- packageRoot,
45
- resolveTemplateDir,
46
- } from "./templates.ts";
40
+ import { DEFAULT_TEMPLATE, TEMPLATES, packageRoot, resolveTemplateDir } from "./templates.ts";
47
41
  import {
48
42
  applyCreateAnswers,
49
43
  sanitizeProjectName,
50
44
  shouldSkipTemplatePath,
51
45
  transformConfigForSqlDriver,
52
46
  transformPackageJson,
53
- transformSchemaForSqlDriver,
54
47
  } from "./transform.ts";
55
48
 
56
49
  describe("defaultsBranchOptions", () => {
@@ -80,12 +73,12 @@ describe("parseArgs", () => {
80
73
  expect(parseArgs(["x", "--template", "standard"]).templateExplicit).toBe(true);
81
74
  });
82
75
 
83
- test("accepts --sql sqlite|postgres", () => {
84
- expect(parseArgs(["x"]).sqlDriver).toBe("sqlite");
76
+ test("accepts --sql postgres (sqlite removed)", () => {
77
+ expect(parseArgs(["x"]).sqlDriver).toBe("postgres");
85
78
  expect(parseArgs(["x"]).sqlDriverExplicit).toBe(false);
86
79
  expect(parseArgs(["x", "--sql", "postgres"]).sqlDriver).toBe("postgres");
87
- expect(parseArgs(["x", "--sql=sqlite"]).sqlDriver).toBe("sqlite");
88
80
  expect(parseArgs(["x", "--sql", "postgres"]).sqlDriverExplicit).toBe(true);
81
+ expect(() => parseArgs(["x", "--sql=sqlite"])).toThrow(/sql/);
89
82
  });
90
83
 
91
84
  test("rejects unknown template, option, and sql", () => {
@@ -124,6 +117,19 @@ describe("parseArgs flags", () => {
124
117
  expect(parseArgs(["x"]).agentsMd).toBe(true);
125
118
  });
126
119
 
120
+ test("accepts --locales ar,fr", () => {
121
+ expect(parseArgs(["x", "--locales", "ar,fr"]).locales).toEqual(["ar", "fr"]);
122
+ expect(parseArgs(["x", "--locales=ar"]).locales).toEqual(["ar"]);
123
+ expect(parseArgs(["x"]).locales).toEqual([]);
124
+ });
125
+
126
+ test("accepts --pgdog / --no-pgdog", () => {
127
+ expect(parseArgs(["x", "--pgdog"]).pgdog).toBe(true);
128
+ expect(parseArgs(["x", "--no-pgdog"]).pgdog).toBe(false);
129
+ expect(parseArgs(["x"]).pgdog).toBeUndefined();
130
+ expect(() => parseArgs(["x", "--pgdog", "--no-pgdog"])).toThrow(/pgdog/);
131
+ });
132
+
127
133
  test("rejects --install with --no-install", () => {
128
134
  expect(() => parseArgs(["x", "--install", "--no-install"])).toThrow(/install/);
129
135
  });
@@ -146,6 +152,8 @@ function recommendedAnswers(overrides: Partial<InteractiveAnswers> = {}): Intera
146
152
  agentsMd: true,
147
153
  createDefaults: undefined,
148
154
  aiApply: null,
155
+ locales: [],
156
+ pgdog: false,
149
157
  ...overrides,
150
158
  };
151
159
  }
@@ -165,8 +173,8 @@ describe("scaffoldArgsFromAnswers ≡ flag-driven", () => {
165
173
  }
166
174
  });
167
175
 
168
- test("interactive never opts into --sql postgres without override", () => {
169
- expect(scaffoldArgsFromAnswers(recommendedAnswers()).sqlDriver).toBe("sqlite");
176
+ test("interactive defaults to postgres SQL", () => {
177
+ expect(scaffoldArgsFromAnswers(recommendedAnswers()).sqlDriver).toBe("postgres");
170
178
  expect(
171
179
  scaffoldArgsFromCli(parseArgs(["x", "--template", "standard", "--sql", "postgres"]))
172
180
  .sqlDriver,
@@ -206,22 +214,20 @@ describe("wizard ← Back", () => {
206
214
  });
207
215
 
208
216
  describe("aiSetupProviderFor", () => {
209
- test("prefers ollama when docker is ollama even if local is mock", () => {
217
+ test("prefers ollama when prod/dev is ollama even if menu is mock", () => {
210
218
  expect(
211
219
  aiSetupProviderFor("mock", {
212
- local: "mock",
213
- docker: "ollama",
220
+ dev: "mock",
214
221
  test: "mock",
215
222
  prod: "ollama",
216
223
  }),
217
224
  ).toBe("ollama");
218
225
  });
219
226
 
220
- test("keeps local ollama", () => {
227
+ test("keeps menu ollama", () => {
221
228
  expect(
222
229
  aiSetupProviderFor("ollama", {
223
- local: "ollama",
224
- docker: "anthropic",
230
+ dev: "ollama",
225
231
  test: "mock",
226
232
  prod: "anthropic",
227
233
  }),
@@ -230,7 +236,7 @@ describe("aiSetupProviderFor", () => {
230
236
  });
231
237
 
232
238
  describe("interactive branches", () => {
233
- test("recommended → no createDefaults, no AI setup", async () => {
239
+ test("recommended → no createDefaults, Docker-first pins, no .oke/mode", async () => {
234
240
  const dir = mkdtempSync(join(tmpdir(), "oke-rec-"));
235
241
  rmSync(dir, { recursive: true, force: true });
236
242
  try {
@@ -241,15 +247,17 @@ describe("interactive branches", () => {
241
247
  });
242
248
  expect(code).toBe(0);
243
249
  const config = readFileSync(join(dir, "oke.config.ts"), "utf8");
244
- expect(config).toContain('local: "sqlite"');
245
- expect(readFileSync(join(dir, ".oke", "mode"), "utf8").trim()).toBe("local");
250
+ expect(config).toMatch(/vault:\s*\{\s*dev: "vault"/);
251
+ expect(config).not.toMatch(/^\s*sql:\s*\{/m);
252
+ expect(existsSync(join(dir, ".oke", "mode"))).toBe(false);
253
+ expect(existsSync(join(dir, "docker", "docker-compose.yml"))).toBe(true);
246
254
  expect(existsSync(join(dir, "src", "flows", "notes", "index.ts"))).toBe(true);
247
255
  } finally {
248
256
  rmSync(dir, { recursive: true, force: true });
249
257
  }
250
258
  });
251
259
 
252
- test("recommended advanced → docker mode + advanced Notes flows", async () => {
260
+ test("recommended advanced → advanced Notes flows + compose", async () => {
253
261
  const dir = mkdtempSync(join(tmpdir(), "oke-rec-adv-"));
254
262
  rmSync(dir, { recursive: true, force: true });
255
263
  try {
@@ -259,10 +267,13 @@ describe("interactive branches", () => {
259
267
  ask: async () => recommendedAnswers({ name: dir, choice: "advanced" }),
260
268
  });
261
269
  expect(code).toBe(0);
262
- expect(readFileSync(join(dir, ".oke", "mode"), "utf8").trim()).toBe("docker");
270
+ expect(existsSync(join(dir, ".oke", "mode"))).toBe(false);
263
271
  const notes = readFileSync(join(dir, "src", "flows", "notes", "index.ts"), "utf8");
264
272
  expect(notes).toContain('flow("notes.digest"');
265
273
  expect(notes).toContain('flow("notes.attach"');
274
+ expect(readFileSync(join(dir, "oke.config.ts"), "utf8")).toMatch(
275
+ /index:\s*\{\s*test:\s*"memory",\s*prod:\s*"meilisearch"/,
276
+ );
266
277
  } finally {
267
278
  rmSync(dir, { recursive: true, force: true });
268
279
  }
@@ -271,7 +282,7 @@ describe("interactive branches", () => {
271
282
  test("reuse previous settings round-trip", async () => {
272
283
  const home = mkdtempSync(join(tmpdir(), "oke-reuse-"));
273
284
  const path = createDefaultsPath(home);
274
- const base = recommendedDefaults("local-only");
285
+ const base = recommendedDefaults("docker-ready");
275
286
  const saved = {
276
287
  ...base,
277
288
  drivers: {
@@ -279,9 +290,8 @@ describe("interactive branches", () => {
279
290
  store: {
280
291
  ...base.drivers.store,
281
292
  sql: {
282
- local: "libsql",
283
- docker: "postgres",
284
- test: "memory",
293
+ dev: "postgres",
294
+ test: "pglite",
285
295
  prod: "postgres",
286
296
  },
287
297
  },
@@ -305,7 +315,7 @@ describe("interactive branches", () => {
305
315
  });
306
316
  expect(code).toBe(0);
307
317
  const config = readFileSync(join(dir, "oke.config.ts"), "utf8");
308
- expect(config).toContain('local: "libsql"');
318
+ expect(config).toMatch(/vault:\s*\{\s*dev: "vault"/);
309
319
  const templateConfig = readFileSync(
310
320
  join(resolveTemplateDir("standard"), "oke.config.ts"),
311
321
  "utf8",
@@ -322,21 +332,23 @@ describe("interactive branches", () => {
322
332
  const path = createDefaultsPath(home);
323
333
  const customized = toCreateDefaults({
324
334
  template: "standard",
325
- profile: "local-only",
335
+ profile: "docker-ready",
326
336
  drivers: {
327
337
  store: {
328
- sql: pinsLocalOnly("pglite", "postgres", "memory"),
329
- kv: pinsLocalOnly("memory", "redis", "memory"),
330
- files: pinsLocalOnly("fs", "s3", "memory"),
338
+ sql: pinsDockerReady("postgres", "pglite"),
339
+ kv: pinsDockerReady("redis", "memory"),
340
+ files: pinsDockerReady("s3", "memory"),
331
341
  index: null,
332
342
  },
333
- signal: pinsLocalOnly("memory", "redis", "memory"),
334
- clock: pinsLocalOnly("memory", "file", "frozen"),
335
- vault: pinsLocalOnly("env", "openbao", "memory"),
336
- channel: { email: pinsLocalOnly("console", "smtp", "console") },
343
+ signal: pinsDockerReady("redis", "memory"),
344
+ clock: pinsDockerReady("postgres", "frozen"),
345
+ vault: pinsDockerReady("vault", "memory"),
346
+ channel: { email: pinsDockerReady("smtp", "console") },
337
347
  ai: null,
338
348
  },
339
349
  ai: { enabled: false, provider: null, driver: null },
350
+ locales: [],
351
+ pgdog: false,
340
352
  });
341
353
 
342
354
  const dir = mkdtempSync(join(tmpdir(), "oke-custom-app-"));
@@ -357,55 +369,17 @@ describe("interactive branches", () => {
357
369
  });
358
370
  expect(code).toBe(0);
359
371
  expect(existsSync(path)).toBe(true);
360
- expect(readCreateDefaults(path)?.drivers.store.sql.local).toBe("pglite");
361
- expect(readFileSync(join(dir, "oke.config.ts"), "utf8")).toContain('local: "pglite"');
362
- // local-only profile → seed oke dev mode so the prompt is not repeated.
363
- expect(readFileSync(join(dir, ".oke", "mode"), "utf8").trim()).toBe("local");
372
+ expect(readCreateDefaults(path)?.drivers.store.sql.dev).toBe("postgres");
373
+ expect(readFileSync(join(dir, "oke.config.ts"), "utf8")).toMatch(
374
+ /vault:\s*\{\s*dev: "vault"/,
375
+ );
376
+ expect(existsSync(join(dir, ".oke", "mode"))).toBe(false);
364
377
  } finally {
365
378
  rmSync(home, { recursive: true, force: true });
366
379
  rmSync(dir, { recursive: true, force: true });
367
380
  }
368
381
  });
369
382
 
370
- test("docker-ready profile seeds .oke/mode as docker", async () => {
371
- const customized = toCreateDefaults({
372
- template: "advanced",
373
- profile: "docker-ready",
374
- drivers: {
375
- store: {
376
- sql: pinsDockerReady("sqlite", "postgres", "memory"),
377
- kv: pinsLocalOnly("memory", "redis", "memory"),
378
- files: pinsLocalOnly("fs", "s3", "memory"),
379
- index: null,
380
- },
381
- signal: pinsLocalOnly("memory", "redis", "memory"),
382
- clock: pinsLocalOnly("memory", "file", "frozen"),
383
- vault: pinsLocalOnly("env", "openbao", "memory"),
384
- channel: { email: pinsLocalOnly("console", "smtp", "console") },
385
- ai: null,
386
- },
387
- ai: { enabled: false, provider: null, driver: null },
388
- });
389
- const dir = mkdtempSync(join(tmpdir(), "oke-docker-mode-"));
390
- rmSync(dir, { recursive: true, force: true });
391
- try {
392
- const code = await run([dir], {
393
- stdinIsTTY: true,
394
- runPostScaffold: false,
395
- ask: async () =>
396
- recommendedAnswers({
397
- name: dir,
398
- createDefaults: customized,
399
- aiApply: null,
400
- }),
401
- });
402
- expect(code).toBe(0);
403
- expect(readFileSync(join(dir, ".oke", "mode"), "utf8").trim()).toBe("docker");
404
- } finally {
405
- rmSync(dir, { recursive: true, force: true });
406
- }
407
- });
408
-
409
383
  test("non-TTY --yes stays zero-prompt recommended", async () => {
410
384
  const dir = mkdtempSync(join(tmpdir(), "oke-yes-"));
411
385
  rmSync(dir, { recursive: true, force: true });
@@ -422,7 +396,7 @@ describe("interactive branches", () => {
422
396
  expect(code).toBe(0);
423
397
  expect(askCalled).toBe(false);
424
398
  const config = readFileSync(join(dir, "oke.config.ts"), "utf8");
425
- expect(config).toContain('local: "sqlite"');
399
+ expect(config).toMatch(/vault:\s*\{\s*dev: "vault"/);
426
400
  expect(config).not.toMatch(/\bai:\s*\{/);
427
401
  } finally {
428
402
  rmSync(dir, { recursive: true, force: true });
@@ -452,45 +426,25 @@ describe("transformPackageJson", () => {
452
426
  });
453
427
  });
454
428
 
455
- describe("transformSchemaForSqlDriver", () => {
456
- const sqliteSchema = `import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
457
- export const pings = sqliteTable("pings", {});
458
- `;
459
-
460
- test("postgres swaps dialect imports and table helper", () => {
461
- const next = transformSchemaForSqlDriver(sqliteSchema, "postgres");
462
- expect(next).toContain('from "drizzle-orm/pg-core"');
463
- expect(next).toContain("pgTable(");
464
- expect(next).not.toContain("sqliteTable");
465
- expect(next).not.toContain("sqlite-core");
466
- });
467
-
468
- test("sqlite is idempotent on template sources", () => {
469
- expect(transformSchemaForSqlDriver(sqliteSchema, "sqlite")).toBe(sqliteSchema);
470
- });
471
- });
472
-
473
429
  describe("transformConfigForSqlDriver", () => {
474
430
  const config = `export default defineConfig({
475
431
  drivers: {
476
432
  store: {
477
433
  sql: {
478
- local: "sqlite",
479
- docker: "postgres",
480
- test: "memory",
481
- prod: "postgres",
434
+ dev: "memory",
435
+ test: "pglite",
436
+ prod: "memory",
482
437
  },
483
438
  },
484
439
  },
485
440
  });
486
441
  `;
487
442
 
488
- test("postgres pins local/docker/prod and leaves test memory", () => {
443
+ test("postgres pins dev/prod and leaves test pglite", () => {
489
444
  const next = transformConfigForSqlDriver(config, "postgres");
490
- expect(next).toContain('local: "postgres"');
491
- expect(next).toContain('docker: "postgres"');
445
+ expect(next).toContain('dev: "postgres"');
492
446
  expect(next).toContain('prod: "postgres"');
493
- expect(next).toContain('test: "memory"');
447
+ expect(next).toContain('test: "pglite"');
494
448
  });
495
449
  });
496
450
 
@@ -517,28 +471,30 @@ describe("formatCdPath", () => {
517
471
  });
518
472
 
519
473
  describe("scaffold structure", () => {
520
- test("each clean template produces its source tree (minus skips)", () => {
474
+ test("each clean template produces its source tree (minus skips)", async () => {
521
475
  for (const id of TEMPLATES) {
522
476
  const dir = mkdtempSync(join(tmpdir(), `create-oke-${id}-`));
523
477
  try {
524
478
  const templateDir = resolveTemplateDir(id);
525
479
  const expected = listTemplateFiles(templateDir);
526
- const result = scaffold({
480
+ const result = await scaffold({
527
481
  targetDir: join(dir, id),
528
482
  name: `app-${id}`,
529
483
  source: { kind: "template", id },
530
484
  });
531
- const extras = ["AGENTS.md", ".oke/mode"];
485
+ const extras = ["AGENTS.md"];
532
486
  if (expected.includes(".env.example")) extras.push(".env.local");
533
- expect([...result.files].sort()).toEqual([...expected, ...extras].sort());
534
- expect(readFileSync(join(result.targetDir, ".oke", "mode"), "utf8").trim()).toBe(
535
- TEMPLATE_DEFAULT_MODE[id],
536
- );
487
+ const composeExtras = result.files.filter((f) => f.startsWith("docker/"));
488
+ expect(composeExtras.length).toBeGreaterThan(0);
489
+ expect(result.files).toContain("docker/docker-compose.yml");
490
+ expect(existsSync(join(result.targetDir, ".oke", "mode"))).toBe(false);
491
+ for (const f of expected) expect(result.files).toContain(f);
492
+ for (const f of extras) expect(result.files).toContain(f);
537
493
  expect(result.files).toContain(".gitignore");
538
494
  expect(result.files).toContain("README.md");
539
495
  expect(result.files).toContain(".github/workflows/ci.yml");
540
496
  expect(result.files).toContain("tsconfig.json");
541
- expect(result.sqlDriver).toBe("sqlite");
497
+ expect(result.sqlDriver).toBe("postgres");
542
498
  expect(readFileSync(join(result.targetDir, "AGENTS.md"), "utf8")).toMatch(
543
499
  /one law|on\(Trigger\)/i,
544
500
  );
@@ -548,7 +504,10 @@ describe("scaffold structure", () => {
548
504
  expect(readme).toMatch(/notes\.(create|attach|digest)|main\.health/);
549
505
  expect(readme).toMatch(/scaffold|Included vs you build/i);
550
506
  expect(readme).toMatch(/\.github\/workflows\/ci\.yml/);
551
- expect(readFileSync(join(result.targetDir, ".gitignore"), "utf8")).toMatch(/node_modules/);
507
+ const gitignore = readFileSync(join(result.targetDir, ".gitignore"), "utf8");
508
+ expect(gitignore).toMatch(/node_modules/);
509
+ expect(gitignore).toMatch(/\.env\.docker/);
510
+ expect(gitignore).not.toMatch(/^\/docker\/compose\.yml$/m);
552
511
  const ciYml = readFileSync(join(result.targetDir, ".github/workflows/ci.yml"), "utf8");
553
512
  expect(() => Bun.YAML.parse(ciYml)).not.toThrow();
554
513
  const ci = Bun.YAML.parse(ciYml) as {
@@ -564,43 +523,53 @@ describe("scaffold structure", () => {
564
523
  const appTs = readFileSync(join(result.targetDir, "src/app.ts"), "utf8");
565
524
  expect(appTs).not.toMatch(/Object\.assign/);
566
525
  expect(appTs).not.toMatch(/env:\s*["']test["']/);
567
- // stores/secrets/signals/channel.templates auto-register — the
568
- // minimal `oke({ name: "notes" })` shape carries no explicit arrays.
569
526
  expect(appTs).not.toMatch(/stores:\s*\[/);
570
527
  expect(appTs).toMatch(/oke\(\{\s*name:\s*["']notes["']\s*\}\)/);
571
528
  const pkg = JSON.parse(readFileSync(join(result.targetDir, "package.json"), "utf8")) as {
572
529
  name: string;
573
530
  dependencies: { okengine: string };
574
531
  scripts: { typecheck?: string; test?: string };
575
- devDependencies: { typescript?: string };
532
+ devDependencies: {
533
+ typescript?: string;
534
+ "@electric-sql/pglite"?: string;
535
+ "@electric-sql/pglite-pgvector"?: string;
536
+ };
576
537
  };
577
538
  expect(pkg.name).toBe(`app-${id}`);
578
539
  expect(pkg.dependencies.okengine).not.toMatch(/^file:\.\./);
579
540
  expect(pkg.scripts.typecheck).toBe("tsc --noEmit");
580
- expect(pkg.scripts.test).toBe("bun test");
541
+ expect(pkg.scripts.test).toBe("oke test");
581
542
  expect(pkg.devDependencies.typescript).toBeTruthy();
543
+ expect(pkg.devDependencies["@electric-sql/pglite"]).toBe("^0.5.4");
544
+ expect(pkg.devDependencies["@electric-sql/pglite-pgvector"]).toBe("^0.0.5");
545
+ const drizzle = readFileSync(join(result.targetDir, "drizzle.config.ts"), "utf8");
546
+ expect(drizzle).toContain('dialect: "postgresql"');
547
+ expect(drizzle).toContain("DATABASE_URL");
548
+ expect(drizzle).toContain("OKE_STORE_SQL_URL");
549
+ expect(drizzle).not.toContain("OKE_SQLITE_URL");
550
+ expect(drizzle).toContain(".oke/schema/oke.ts");
551
+ expect(existsSync(join(result.targetDir, ".oke/schema/oke.ts"))).toBe(true);
552
+ const sqlYml = readFileSync(join(result.targetDir, "docker/docker-compose.yml"), "utf8");
553
+ expect(sqlYml).toMatch(/healthcheck/);
554
+ expect(sqlYml).toMatch(/pg_isready/);
582
555
  } finally {
583
556
  rmSync(dir, { recursive: true, force: true });
584
557
  }
585
558
  }
586
559
  });
587
560
 
588
- test("standard has Notes layout (gates/vault/channels + notes flows)", () => {
561
+ test("standard has Notes layout (core.ts + notes flows)", async () => {
589
562
  const dir = mkdtempSync(join(tmpdir(), "create-oke-standard-assert-"));
590
563
  try {
591
- const result = scaffold({
564
+ const result = await scaffold({
592
565
  targetDir: join(dir, "standard"),
593
566
  name: "standard-app",
594
567
  source: { kind: "template", id: "standard" },
595
568
  });
596
569
  for (const path of [
597
- "src/core/gates.ts",
598
- "src/core/vault.ts",
599
- "src/core/channels.ts",
600
- "src/core/store.ts",
601
- "src/core/index.ts",
570
+ "src/core.ts",
602
571
  "src/locales/en.ts",
603
- "src/locales/ar.ts",
572
+ "src/locales/index.ts",
604
573
  "src/flows/main/shapes.ts",
605
574
  "src/flows/main/signals.ts",
606
575
  "src/flows/notes/index.ts",
@@ -614,6 +583,19 @@ describe("scaffold structure", () => {
614
583
  const notes = readFileSync(join(result.targetDir, "src/flows/notes/index.ts"), "utf8");
615
584
  expect(notes).toContain('flow("notes.create"');
616
585
  expect(notes).not.toContain('flow("notes.digest"');
586
+ expect(existsSync(join(result.targetDir, "src/locales/ar.ts"))).toBe(false);
587
+ expect(readFileSync(join(result.targetDir, "oke.config.ts"), "utf8")).toContain(
588
+ 'locales: ["en"]',
589
+ );
590
+ expect(readFileSync(join(result.targetDir, "oke.config.ts"), "utf8")).not.toMatch(
591
+ /^\s*pgdog:\s*"/m,
592
+ );
593
+ const appTsStandard = readFileSync(join(result.targetDir, "src/app.ts"), "utf8");
594
+ expect(appTsStandard).toContain('import "@/core"');
595
+ expect(appTsStandard).not.toContain('import "@/locales/');
596
+ expect(readFileSync(join(result.targetDir, "src/core.ts"), "utf8")).toContain(
597
+ 'import "@/locales"',
598
+ );
617
599
  const all = result.files
618
600
  .filter((f) => f.endsWith(".ts") || f.endsWith(".md"))
619
601
  .map((f) => readFileSync(join(result.targetDir, f), "utf8"))
@@ -624,38 +606,96 @@ describe("scaffold structure", () => {
624
606
  }
625
607
  });
626
608
 
627
- test("--sql postgres pins store.sql (abstract schema stays dialect-agnostic)", () => {
609
+ test("locales ar,fr adds catalogs and keeps en default", async () => {
610
+ const dir = mkdtempSync(join(tmpdir(), "create-oke-locales-"));
611
+ try {
612
+ const result = await scaffold({
613
+ targetDir: join(dir, "i18n-app"),
614
+ name: "i18n-app",
615
+ source: { kind: "template", id: "standard" },
616
+ locales: ["ar", "fr"],
617
+ });
618
+ expect(result.locales).toEqual(["ar", "fr"]);
619
+ expect(existsSync(join(result.targetDir, "src/locales/ar.ts"))).toBe(true);
620
+ expect(existsSync(join(result.targetDir, "src/locales/fr.ts"))).toBe(true);
621
+ const config = readFileSync(join(result.targetDir, "oke.config.ts"), "utf8");
622
+ expect(config).toContain('"ar"');
623
+ expect(config).toContain('"fr"');
624
+ expect(config).toContain('"ar": "rtl"');
625
+ const app = readFileSync(join(result.targetDir, "src/app.ts"), "utf8");
626
+ expect(app).toContain('import "@/core"');
627
+ expect(app).not.toContain('import "@/locales/');
628
+ const localesIndex = readFileSync(join(result.targetDir, "src/locales/index.ts"), "utf8");
629
+ expect(localesIndex).toContain('import "./en";');
630
+ expect(localesIndex).toContain('import "./ar";');
631
+ expect(localesIndex).toContain('import "./fr";');
632
+ } finally {
633
+ rmSync(dir, { recursive: true, force: true });
634
+ }
635
+ });
636
+
637
+ test("pgdog opt-in pins images.pgdog and compose service", async () => {
638
+ const dir = mkdtempSync(join(tmpdir(), "create-oke-pgdog-"));
639
+ try {
640
+ const off = await scaffold({
641
+ targetDir: join(dir, "no-pool"),
642
+ name: "no-pool",
643
+ source: { kind: "template", id: "standard" },
644
+ pgdog: false,
645
+ });
646
+ expect(off.pgdog).toBe(false);
647
+ expect(readFileSync(join(off.targetDir, "oke.config.ts"), "utf8")).not.toMatch(
648
+ /^\s*pgdog:\s*"/m,
649
+ );
650
+
651
+ const on = await scaffold({
652
+ targetDir: join(dir, "with-pool"),
653
+ name: "with-pool",
654
+ source: { kind: "template", id: "standard" },
655
+ pgdog: true,
656
+ });
657
+ expect(on.pgdog).toBe(true);
658
+ const config = readFileSync(join(on.targetDir, "oke.config.ts"), "utf8");
659
+ expect(config).toMatch(/pgdog:\s*"ghcr\.io\/pgdogdev\/pgdog:/);
660
+ const compose = readFileSync(join(on.targetDir, "docker/docker-compose.yml"), "utf8");
661
+ expect(compose).toContain("pgdog:");
662
+ expect(existsSync(join(on.targetDir, "docker/pgdog/pgdog.toml"))).toBe(true);
663
+ } finally {
664
+ rmSync(dir, { recursive: true, force: true });
665
+ }
666
+ });
667
+
668
+ test("--sql postgres pins store.sql (abstract schema stays dialect-agnostic)", async () => {
628
669
  const dir = mkdtempSync(join(tmpdir(), "create-oke-sql-pg-"));
629
670
  try {
630
- const result = scaffold({
671
+ const result = await scaffold({
631
672
  targetDir: join(dir, "pg-app"),
632
673
  name: "pg-app",
633
674
  source: { kind: "template", id: "standard" },
634
675
  sqlDriver: "postgres",
635
676
  });
636
677
  expect(result.sqlDriver).toBe("postgres");
637
- // Abstract decl is dialect-agnostic — emit picks pgTable at sync time.
638
678
  const decl = readFileSync(join(result.targetDir, "src/db/schema.decl.ts"), "utf8");
639
679
  expect(decl).toContain("store.schema.table(");
640
680
  expect(decl).not.toContain("sqliteTable");
641
681
  expect(decl).not.toContain("pgTable");
642
682
  const drizzle = readFileSync(join(result.targetDir, "drizzle.config.ts"), "utf8");
643
- expect(drizzle).toContain("OKE_DRIZZLE_DIALECT");
644
- expect(drizzle).toContain("schema.generated.ts");
683
+ expect(drizzle).toContain('dialect: "postgresql"');
684
+ expect(drizzle).toContain("schema.drizzle.ts");
685
+ expect(drizzle).toContain(".oke/schema/oke.ts");
645
686
  const config = readFileSync(join(result.targetDir, "oke.config.ts"), "utf8");
646
- expect(config).toMatch(/sql:\s*\{[\s\S]*local:\s*"postgres"/);
647
- expect(config).toMatch(/sql:\s*\{[\s\S]*docker:\s*"postgres"/);
648
- expect(config).toMatch(/sql:\s*\{[\s\S]*prod:\s*"postgres"/);
649
- expect(config).toMatch(/sql:\s*\{[\s\S]*test:\s*"memory"/);
687
+ // postgres is DRIVER_DEFAULTS — sparse templates omit the sql map entirely.
688
+ expect(config).not.toMatch(/^\s*sql:\s*\{/m);
689
+ expect(config).toMatch(/vault:\s*\{\s*dev: "vault"/);
650
690
  } finally {
651
691
  rmSync(dir, { recursive: true, force: true });
652
692
  }
653
693
  });
654
694
 
655
- test("--no-agents-md skips AGENTS.md", () => {
695
+ test("--no-agents-md skips AGENTS.md", async () => {
656
696
  const dir = mkdtempSync(join(tmpdir(), "create-oke-no-agents-"));
657
697
  try {
658
- const result = scaffold({
698
+ const result = await scaffold({
659
699
  targetDir: join(dir, "standard"),
660
700
  name: "no-agents",
661
701
  source: { kind: "template", id: "standard" },
@@ -742,7 +782,6 @@ describe("non-TTY CLI", () => {
742
782
  const err = await new Response(proc.stderr).text();
743
783
  expect(code).toBe(1);
744
784
  expect(err).toMatch(/missing <name>/);
745
- // Clack intro / select chrome must not appear.
746
785
  expect(out + err).not.toMatch(/Project name|Start from a worked example/);
747
786
  expect(out + err).not.toMatch(/│/);
748
787
  });
@@ -765,7 +804,9 @@ describe("non-TTY CLI", () => {
765
804
  label: "standard",
766
805
  okengineDependency: "x",
767
806
  files: [],
768
- sqlDriver: "sqlite" as const,
807
+ sqlDriver: "postgres" as const,
808
+ locales: [],
809
+ pgdog: false,
769
810
  };
770
811
  expect(nextStepsText(result)).toContain("oke dev");
771
812
  expect(nextStepsText(result)).toContain("bun install");