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.ts CHANGED
@@ -39,6 +39,7 @@ import {
39
39
  } from "./ai-setup/from-pref.ts";
40
40
  import { askAiSetup } from "./ai-setup/prompts.ts";
41
41
  import { askCustomizeFlow } from "./customize-flow.ts";
42
+ import { parseExtraLocales } from "./locales.ts";
42
43
  import {
43
44
  scaffold,
44
45
  targetDirectoryBlockReason,
@@ -65,7 +66,7 @@ export type CliArgs = {
65
66
  readonly template: TemplateId;
66
67
  /** True when `--template` / `-t` was present on the argv. */
67
68
  readonly templateExplicit: boolean;
68
- /** Store SQL driver (`sqlite` default). */
69
+ /** Store SQL driver (`postgres` default). */
69
70
  readonly sqlDriver: SqlDriverId;
70
71
  /** True when `--sql` was present on the argv. */
71
72
  readonly sqlDriverExplicit: boolean;
@@ -81,6 +82,13 @@ export type CliArgs = {
81
82
  readonly agentsMd: boolean;
82
83
  /** AI setup after install. */
83
84
  readonly ai: AiCliMode;
85
+ /** Extra locales beyond English (`--locales ar,fr`). */
86
+ readonly locales: readonly string[];
87
+ /**
88
+ * PgDog pooling flag from CLI.
89
+ * `undefined` = ask (interactive) / off (`--yes`).
90
+ */
91
+ readonly pgdog: boolean | undefined;
84
92
  readonly targetDir: string | undefined;
85
93
  };
86
94
 
@@ -111,8 +119,8 @@ export function defaultsBranchOptions(
111
119
  label: "Yes, use recommended defaults",
112
120
  hint:
113
121
  template === "advanced"
114
- ? "Notes · docker-ready pins · .oke/mode docker"
115
- : "Notes · local-first pins · .oke/mode local",
122
+ ? "Notes · Docker-first pins · store.index"
123
+ : "Notes · Docker-first pins (postgres · redis · s3)",
116
124
  },
117
125
  ];
118
126
  if (hasPreviousForTemplate) {
@@ -125,10 +133,7 @@ export function defaultsBranchOptions(
125
133
  options.push({
126
134
  value: "customize",
127
135
  label: "No, customize settings",
128
- hint:
129
- template === "standard"
130
- ? "local|docker · SQL · optional AI"
131
- : "local|docker · all facets · optional AI",
136
+ hint: template === "standard" ? "dev/prod SQL · optional AI" : "dev/prod facets · optional AI",
132
137
  });
133
138
  return options;
134
139
  }
@@ -143,6 +148,10 @@ export type InteractiveAnswers = {
143
148
  readonly createDefaults: CreateDefaults | undefined;
144
149
  /** Model / env apply payload — written after scaffold, before install. */
145
150
  readonly aiApply: AiSetupApplyInput | null;
151
+ /** Extra locales beyond English (empty = English only). */
152
+ readonly locales: readonly string[];
153
+ /** Pin PgDog in front of Postgres. */
154
+ readonly pgdog: boolean;
146
155
  };
147
156
 
148
157
  /**
@@ -155,8 +164,12 @@ export type ScaffoldCallArgs = {
155
164
  readonly agentsMd: boolean;
156
165
  readonly sqlDriver: SqlDriverId;
157
166
  readonly createDefaults?: CreateDefaults;
158
- /** Apply `src/core/ai.ts` + `.env.local` after scaffold (before install). */
167
+ /** Apply AI models in `src/core.ts` + `.env.local` after scaffold (before install). */
159
168
  readonly aiApply?: AiSetupApplyInput | null;
169
+ /** Extra locales beyond English. */
170
+ readonly locales?: readonly string[];
171
+ /** Pin PgDog in front of Postgres. */
172
+ readonly pgdog?: boolean;
160
173
  };
161
174
 
162
175
  /**
@@ -175,6 +188,8 @@ export function parseArgs(argv: readonly string[]): CliArgs {
175
188
  let install: boolean | undefined;
176
189
  let agentsMd = true;
177
190
  let ai: AiCliMode = "prompt";
191
+ let locales: readonly string[] = [];
192
+ let pgdog: boolean | undefined;
178
193
  let targetDir: string | undefined;
179
194
 
180
195
  for (let i = 0; i < argv.length; i++) {
@@ -259,6 +274,33 @@ export function parseArgs(argv: readonly string[]): CliArgs {
259
274
  templateExplicit = true;
260
275
  continue;
261
276
  }
277
+ if (a === "--locales") {
278
+ const next = argv[++i];
279
+ if (!next) throw new Error("create-oke: --locales requires a value (e.g. ar or ar,fr)");
280
+ const parsed = parseExtraLocales(next);
281
+ if (parsed === null) {
282
+ throw new Error("create-oke: --locales must be BCP-47 tags like ar or ar,fr");
283
+ }
284
+ locales = parsed;
285
+ continue;
286
+ }
287
+ if (a.startsWith("--locales=")) {
288
+ const value = a.slice("--locales=".length);
289
+ const parsed = parseExtraLocales(value);
290
+ if (parsed === null) {
291
+ throw new Error("create-oke: --locales must be BCP-47 tags like ar or ar,fr");
292
+ }
293
+ locales = parsed;
294
+ continue;
295
+ }
296
+ if (a === "--pgdog") {
297
+ pgdog = true;
298
+ continue;
299
+ }
300
+ if (a === "--no-pgdog") {
301
+ pgdog = false;
302
+ continue;
303
+ }
262
304
  if (a.startsWith("-")) {
263
305
  throw new Error(`create-oke: unknown option ${a}`);
264
306
  }
@@ -272,6 +314,9 @@ export function parseArgs(argv: readonly string[]): CliArgs {
272
314
  if (argv.includes("--install") && argv.includes("--no-install")) {
273
315
  throw new Error("create-oke: use either --install or --no-install, not both");
274
316
  }
317
+ if (argv.includes("--pgdog") && argv.includes("--no-pgdog")) {
318
+ throw new Error("create-oke: use either --pgdog or --no-pgdog, not both");
319
+ }
275
320
 
276
321
  if (name !== undefined) {
277
322
  targetDir = resolve(name);
@@ -288,6 +333,8 @@ export function parseArgs(argv: readonly string[]): CliArgs {
288
333
  install,
289
334
  agentsMd,
290
335
  ai,
336
+ locales,
337
+ pgdog,
291
338
  targetDir,
292
339
  };
293
340
  }
@@ -316,7 +363,8 @@ Next steps:
316
363
 
317
364
  cd ${formatCdPath(result.targetDir)}
318
365
  bun install
319
- oke dev # app :6530 · Console :6533 · MCP :6535
366
+ oke schema generate # system stubs .oke/schema/oke.ts (also runs on db push)
367
+ oke dev # app :6530 · Console :6533 · MCP :6535
320
368
 
321
369
  Docs: ${docsUrl("/docs")}
322
370
  `;
@@ -339,9 +387,8 @@ Usage:
339
387
 
340
388
  Options:
341
389
  -t, --template <id> Clean starter (default: ${DEFAULT_TEMPLATE})
342
- --sql <id> Opt-in Store SQL dialect: ${SQL_DRIVERS.join("|")}
343
- Default keeps local sqlite · docker/prod postgres.
344
- --sql postgres pins store.sql local/docker/prod to postgres.
390
+ --sql <id> Store SQL dialect: ${SQL_DRIVERS.join("|")} (default)
391
+ Pins store.sql dev/prod (test stays pglite).
345
392
  -y, --yes No prompts; defaults + bun install (no oke dev)
346
393
  --install Run bun install after scaffold
347
394
  --no-install Skip bun install
@@ -349,15 +396,19 @@ Options:
349
396
  --no-agents-md Skip AGENTS.md
350
397
  --ai Configure AI in the wizard (models before install)
351
398
  --no-ai, --ai skip Skip AI configuration
399
+ --locales <tags> Extra languages beyond English (e.g. ar or ar,fr)
400
+ --pgdog Pin PgDog pooling in front of Postgres
401
+ --no-pgdog Skip PgDog (default for --yes)
352
402
  -h, --help Show this help
353
403
 
354
404
  Template:
355
405
  ${templateLines}
356
406
 
357
407
  On a TTY: pick standard|advanced, then recommended defaults, customize
358
- (local or docker first; store.index with none; AI setup Recommended /
359
- Customize / Off; optional other side; saved to ~/.oke/create-defaults.json),
360
- or reuse when saved for that template. Non-TTY / --yes stay zero-prompt.
408
+ (Docker-first facets; store.index with none; AI setup Recommended /
409
+ Customize / Off; saved to ~/.oke/create-defaults.json),
410
+ optional extra locales + PgDog pooling, or reuse when saved for that template.
411
+ Non-TTY / --yes stay English-only / no PgDog unless --locales / --pgdog.
361
412
  `;
362
413
  }
363
414
 
@@ -400,6 +451,8 @@ export function scaffoldArgsFromCli(args: CliArgs): ScaffoldCallArgs {
400
451
  agentsMd: args.agentsMd,
401
452
  sqlDriver: args.sqlDriver,
402
453
  aiApply: args.ai === "force" ? nonInteractiveAiApply("llama-cpp") : null,
454
+ locales: args.locales,
455
+ pgdog: args.pgdog ?? false,
403
456
  };
404
457
  }
405
458
 
@@ -426,8 +479,7 @@ export function scaffoldArgsFromAnswers(
426
479
  store: {
427
480
  ...createDefaults.drivers.store,
428
481
  sql: {
429
- local: "postgres",
430
- docker: "postgres",
482
+ dev: "postgres",
431
483
  test: createDefaults.drivers.store.sql.test,
432
484
  prod: "postgres",
433
485
  },
@@ -437,7 +489,7 @@ export function scaffoldArgsFromAnswers(
437
489
  sqlDriver = "postgres";
438
490
  } else if (!createDefaults && sqlDriverOverride) {
439
491
  sqlDriver = sqlDriverOverride;
440
- } else if (createDefaults?.drivers.store.sql.local === "postgres") {
492
+ } else if (createDefaults?.drivers.store.sql.dev === "postgres") {
441
493
  sqlDriver = "postgres";
442
494
  }
443
495
 
@@ -449,6 +501,8 @@ export function scaffoldArgsFromAnswers(
449
501
  sqlDriver,
450
502
  ...(createDefaults !== undefined ? { createDefaults } : {}),
451
503
  aiApply: answers.aiApply,
504
+ locales: answers.locales,
505
+ pgdog: answers.pgdog,
452
506
  };
453
507
  }
454
508
 
@@ -463,6 +517,8 @@ export type AskInteractiveFn = (partial: {
463
517
  readonly sqlDriver?: SqlDriverId;
464
518
  readonly ai?: AiCliMode;
465
519
  readonly template?: TemplateId;
520
+ readonly locales?: readonly string[];
521
+ readonly pgdog?: boolean;
466
522
  }) => Promise<InteractiveAnswers | null>;
467
523
 
468
524
  /** Injectable persistence seams for tests. */
@@ -486,6 +542,8 @@ export async function askInteractiveAnswers(
486
542
  readonly sqlDriver?: SqlDriverId;
487
543
  readonly ai?: AiCliMode;
488
544
  readonly template?: TemplateId;
545
+ readonly locales?: readonly string[];
546
+ readonly pgdog?: boolean;
489
547
  } = {},
490
548
  io: CreateDefaultsIo = {
491
549
  path: createDefaultsPath(),
@@ -523,6 +581,7 @@ export async function askInteractiveAnswers(
523
581
 
524
582
  let createDefaults: CreateDefaults | undefined;
525
583
  let aiApply: AiSetupApplyInput | null = null;
584
+ let persistDefaults = false;
526
585
 
527
586
  for (;;) {
528
587
  const previous = io.read();
@@ -570,8 +629,7 @@ export async function askInteractiveAnswers(
570
629
  if (customized === null) return null;
571
630
  if (customized === WIZARD_BACK) continue;
572
631
  createDefaults = customized;
573
- io.write(customized);
574
- note(`Saved globally for next projects → ${io.path}`, "Defaults");
632
+ persistDefaults = true;
575
633
  if (customized.ai.enabled && partial.ai !== "skip") {
576
634
  aiApply = applyInputFromAiPref(customized.ai);
577
635
  }
@@ -586,6 +644,39 @@ export async function askInteractiveAnswers(
586
644
  aiApply = picked;
587
645
  }
588
646
 
647
+ const locales = await askExtraLocales(partial.locales ?? createDefaults?.locales ?? []);
648
+ if (locales === null) return null;
649
+
650
+ const usesPostgres = createDefaults
651
+ ? createDefaults.drivers.store.sql.dev === "postgres" ||
652
+ createDefaults.drivers.store.sql.prod === "postgres" ||
653
+ createDefaults.drivers.store.sql.dev === "pgvector" ||
654
+ createDefaults.drivers.store.sql.prod === "pgvector"
655
+ : true;
656
+ let pgdog = false;
657
+ if (usesPostgres) {
658
+ if (partial.pgdog !== undefined) {
659
+ pgdog = partial.pgdog;
660
+ } else {
661
+ const picked = await askPgDog(createDefaults?.pgdog ?? false);
662
+ if (picked === null) return null;
663
+ pgdog = picked;
664
+ }
665
+ }
666
+
667
+ if (createDefaults) {
668
+ createDefaults = {
669
+ ...createDefaults,
670
+ locales,
671
+ pgdog,
672
+ updatedAt: new Date().toISOString(),
673
+ };
674
+ }
675
+ if (persistDefaults && createDefaults) {
676
+ io.write(createDefaults);
677
+ note(`Saved globally for next projects → ${io.path}`, "Defaults");
678
+ }
679
+
589
680
  const agentsMd = partial.agentsMd ?? true;
590
681
 
591
682
  const installAndRunValue = await confirm({
@@ -601,25 +692,77 @@ export async function askInteractiveAnswers(
601
692
  agentsMd,
602
693
  createDefaults,
603
694
  aiApply,
695
+ locales,
696
+ pgdog,
604
697
  };
605
698
  }
606
699
 
700
+ /**
701
+ * Ask whether to add languages beyond English, then collect BCP-47 tags.
702
+ *
703
+ * @param initialExtra - Prefill from reused create-defaults
704
+ * @returns Extra locale tags, or `null` on cancel
705
+ */
706
+ async function askExtraLocales(
707
+ initialExtra: readonly string[] = [],
708
+ ): Promise<readonly string[] | null> {
709
+ const addMore = await confirm({
710
+ message: "Add more languages besides English?",
711
+ initialValue: initialExtra.length > 0,
712
+ });
713
+ if (isCancel(addMore)) return null;
714
+ if (!addMore) return [];
715
+
716
+ const raw = await text({
717
+ message: "Language codes (comma-separated)",
718
+ placeholder: "ar",
719
+ initialValue: initialExtra.length > 0 ? initialExtra.join(",") : "ar",
720
+ validate: (value) => {
721
+ if (!value?.trim()) return "Enter at least one code (e.g. ar or ar,fr)";
722
+ if (parseExtraLocales(value) === null) {
723
+ return "Use BCP-47 tags like ar or ar,fr";
724
+ }
725
+ return undefined;
726
+ },
727
+ });
728
+ if (isCancel(raw)) return null;
729
+ return parseExtraLocales(String(raw)) ?? [];
730
+ }
731
+
732
+ /**
733
+ * Ask whether to put PgDog in front of Postgres.
734
+ *
735
+ * @param initial - Prefill from reused create-defaults
736
+ * @returns `true` to pin PgDog, or `null` on cancel
737
+ */
738
+ async function askPgDog(initial: boolean = false): Promise<boolean | null> {
739
+ const value = await confirm({
740
+ message: "Add PgDog connection pooling in front of Postgres?",
741
+ initialValue: initial,
742
+ });
743
+ if (isCancel(value)) return null;
744
+ return Boolean(value);
745
+ }
746
+
607
747
  /**
608
748
  * Provider passed to `oke ai setup` — prefer native Ollama when either env uses it.
609
749
  *
610
- * @param localProvider - Menu id chosen for local
750
+ * @param menuProvider - Menu id chosen in the wizard
611
751
  * @param pins - Resolved driver pins
612
752
  */
613
- export function aiSetupProviderFor(localProvider: string, pins: EnvDriverPins): string {
614
- if (localProvider === "ollama" || pins.local === "ollama") return "ollama";
615
- if (pins.docker === "ollama") return "ollama";
616
- if (localProvider === "llama-cpp") return "llama-cpp";
617
- if (localProvider === "vllm" || localProvider === "sglang") return localProvider;
618
- if (localProvider === "mock") {
619
- if (pins.docker === "anthropic") return "anthropic";
620
- if (pins.docker === "openai-compatible") return "llama-cpp";
753
+ export function aiSetupProviderFor(menuProvider: string, pins: EnvDriverPins): string {
754
+ if (menuProvider === "ollama" || pins.dev === "ollama" || pins.prod === "ollama") {
755
+ return "ollama";
756
+ }
757
+ if (menuProvider === "llama-cpp") return "llama-cpp";
758
+ if (menuProvider === "vllm" || menuProvider === "sglang") return menuProvider;
759
+ if (menuProvider === "mock") {
760
+ if (pins.dev === "anthropic" || pins.prod === "anthropic") return "anthropic";
761
+ if (pins.dev === "openai-compatible" || pins.prod === "openai-compatible") {
762
+ return "llama-cpp";
763
+ }
621
764
  }
622
- return localProvider;
765
+ return menuProvider;
623
766
  }
624
767
 
625
768
  /**
@@ -728,6 +871,8 @@ async function runInteractive(
728
871
  agentsMd: args.agentsMd,
729
872
  sqlDriver: args.sqlDriverExplicit ? args.sqlDriver : undefined,
730
873
  ai: args.ai,
874
+ locales: args.locales,
875
+ ...(args.pgdog !== undefined ? { pgdog: args.pgdog } : {}),
731
876
  });
732
877
  if (answers === null) {
733
878
  cancel("Cancelled.");
@@ -764,6 +909,8 @@ async function runScaffold(
764
909
  sqlDriver,
765
910
  createDefaults,
766
911
  aiApply,
912
+ locales,
913
+ pgdog,
767
914
  interactive,
768
915
  install,
769
916
  startDev,
@@ -791,17 +938,19 @@ async function runScaffold(
791
938
  spun = spinner();
792
939
  spun.start("Scaffolding…");
793
940
  }
794
- const result = scaffold({
941
+ const result = await scaffold({
795
942
  targetDir,
796
943
  name,
797
944
  source,
798
945
  writeAgentsMd: agentsMd,
799
946
  sqlDriver,
800
947
  ...(createDefaults !== undefined ? { createDefaults } : {}),
948
+ ...(locales !== undefined ? { locales } : {}),
949
+ ...(pgdog !== undefined ? { pgdog } : {}),
801
950
  });
802
951
  if (spun) spun.stop("Scaffolded.");
803
952
 
804
- // Models were chosen in the wizard — write env + src/core/ai.ts before install
953
+ // Models were chosen in the wizard — write env + AI models in src/core.ts before install
805
954
  // so `--no-install` still gets a complete AI project. Preserve per-env
806
955
  // `drivers.ai` pins from customize; flatten only when pins were never written.
807
956
  if (runPostScaffold && aiApply) {
@@ -829,6 +978,15 @@ async function runScaffold(
829
978
  return 1;
830
979
  }
831
980
  if (interactive) log.success("Installed.");
981
+ // System stubs under `.oke/schema/` — refresh after install so published
982
+ // create-oke (no monorepo import) still gets them; re-run after plugins.
983
+ const schemaOk = await runCommand(["bunx", "oke", "schema", "generate"], targetDir, {
984
+ inherit: false,
985
+ });
986
+ if (interactive) {
987
+ if (schemaOk) log.success("System schema generated (.oke/schema/oke.ts).");
988
+ else log.warn("oke schema generate skipped — run it after plugins / auth tables.");
989
+ }
832
990
  }
833
991
 
834
992
  const message = nextStepsText(result);
@@ -13,7 +13,7 @@ import {
13
13
  toCreateDefaults,
14
14
  writeCreateDefaults,
15
15
  } from "./create-defaults.ts";
16
- import { pinsLocalOnly, recommendedDefaults } from "./drivers-catalog.ts";
16
+ import { pinsDockerReady, recommendedDefaults } from "./drivers-catalog.ts";
17
17
 
18
18
  describe("createDefaultsPath", () => {
19
19
  test("joins ~/.oke/create-defaults.json", () => {
@@ -26,19 +26,21 @@ describe("parseCreateDefaults", () => {
26
26
  expect(parseCreateDefaults(null)).toBeNull();
27
27
  expect(parseCreateDefaults({ version: 2 })).toBeNull();
28
28
  expect(parseCreateDefaults({ version: 1, profile: "nope" })).toBeNull();
29
+ expect(parseCreateDefaults({ version: 1, profile: "local-only" })).toBeNull();
29
30
  });
30
31
 
31
32
  test("accepts recommended shape", () => {
32
- const d = recommendedDefaults("local-only", "standard");
33
+ const d = recommendedDefaults("docker-ready", "standard");
33
34
  const parsed = parseCreateDefaults(d);
34
- expect(parsed?.profile).toBe("local-only");
35
+ expect(parsed?.profile).toBe("docker-ready");
35
36
  expect(parsed?.template).toBe("standard");
36
- expect(parsed?.drivers.store.sql.local).toBe("sqlite");
37
+ expect(parsed?.drivers.store.sql.dev).toBe("postgres");
38
+ expect(parsed?.drivers.store.sql.test).toBe("pglite");
37
39
  expect(parsed?.ai.enabled).toBe(false);
38
40
  });
39
41
 
40
42
  test("older files without template default to standard", () => {
41
- const d = recommendedDefaults("local-only", "standard");
43
+ const d = recommendedDefaults("docker-ready", "standard");
42
44
  const { template: _t, ...rest } = d;
43
45
  const parsed = parseCreateDefaults(rest);
44
46
  expect(parsed?.template).toBe("standard");
@@ -55,27 +57,31 @@ describe("read/write round-trip", () => {
55
57
  profile: "docker-ready",
56
58
  drivers: {
57
59
  store: {
58
- sql: pinsLocalOnly("libsql", "postgres", "memory"),
59
- kv: pinsLocalOnly("redis", "redis", "memory"),
60
- files: pinsLocalOnly("fs", "s3", "memory"),
60
+ sql: pinsDockerReady("postgres", "pglite"),
61
+ kv: pinsDockerReady("redis", "memory"),
62
+ files: pinsDockerReady("s3", "memory"),
61
63
  index: null,
62
64
  },
63
- signal: pinsLocalOnly("memory", "redis", "memory"),
64
- clock: pinsLocalOnly("file", "file", "frozen"),
65
- vault: pinsLocalOnly("env", "openbao", "memory"),
66
- channel: { email: pinsLocalOnly("console", "smtp", "console") },
67
- ai: pinsLocalOnly("ollama", "ollama", "mock"),
65
+ signal: pinsDockerReady("redis", "memory"),
66
+ clock: pinsDockerReady("file", "frozen"),
67
+ vault: pinsDockerReady("vault", "memory"),
68
+ channel: { email: pinsDockerReady("smtp", "console") },
69
+ ai: pinsDockerReady("ollama", "mock"),
68
70
  },
69
71
  ai: { enabled: true, provider: "ollama", driver: "ollama" },
72
+ locales: ["ar"],
73
+ pgdog: true,
70
74
  });
71
75
  writeCreateDefaults(original, path);
72
76
  const loaded = readCreateDefaults(path);
73
77
  expect(loaded).not.toBeNull();
74
78
  expect(loaded!.template).toBe("advanced");
75
- expect(loaded!.drivers.store.sql.local).toBe("libsql");
76
- expect(loaded!.drivers.ai?.local).toBe("ollama");
79
+ expect(loaded!.drivers.store.sql.dev).toBe("postgres");
80
+ expect(loaded!.drivers.ai?.dev).toBe("ollama");
77
81
  expect(loaded!.ai.provider).toBe("ollama");
78
82
  expect(loaded!.profile).toBe("docker-ready");
83
+ expect(loaded!.locales).toEqual(["ar"]);
84
+ expect(loaded!.pgdog).toBe(true);
79
85
  } finally {
80
86
  rmSync(home, { recursive: true, force: true });
81
87
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * User-global create-oke defaults — `~/.oke/create-defaults.json`.
3
3
  *
4
- * Project-local `.oke/` (mode, sqlite, …) stays under the app cwd.
4
+ * Project-local `.oke/` stays under the app cwd.
5
5
  * This file is the first home-directory preference for the scaffold wizard.
6
6
  */
7
7
 
@@ -10,13 +10,12 @@ import { homedir } from "node:os";
10
10
  import { dirname, join } from "node:path";
11
11
  import { isTemplateId, type TemplateId } from "./templates.ts";
12
12
 
13
- /** Create profile assumption mirrors `oke dev` local vs docker-ready. */
14
- export type CreateProfile = "local-only" | "docker-ready";
13
+ /** Create profile — Docker Compose is always the `dev` runtime. */
14
+ export type CreateProfile = "docker-ready";
15
15
 
16
16
  /** Env columns written into `oke.config.ts` driver maps. */
17
17
  export type EnvDriverPins = {
18
- readonly local: string;
19
- readonly docker: string;
18
+ readonly dev: string;
20
19
  readonly test: string;
21
20
  readonly prod: string;
22
21
  };
@@ -62,6 +61,16 @@ export type CreateDefaults = {
62
61
  readonly ai: EnvDriverPins | null;
63
62
  };
64
63
  readonly ai: CreateAiPref;
64
+ /**
65
+ * Extra locales beyond English (`en` is always the default).
66
+ * Empty = English-only project.
67
+ */
68
+ readonly locales: readonly string[];
69
+ /**
70
+ * Pin `images.pgdog` so Compose puts PgDog in front of Postgres.
71
+ * Default false — opt in via the wizard or `--pgdog`.
72
+ */
73
+ readonly pgdog: boolean;
65
74
  readonly updatedAt: string;
66
75
  };
67
76
 
@@ -130,7 +139,8 @@ export function parseCreateDefaults(raw: unknown): CreateDefaults | null {
130
139
  if (!raw || typeof raw !== "object") return null;
131
140
  const o = raw as Record<string, unknown>;
132
141
  if (o.version !== 1) return null;
133
- if (o.profile !== "local-only" && o.profile !== "docker-ready") return null;
142
+ // Reject deprecated local-only / unknown profiles (Docker-first only).
143
+ if (o.profile !== "docker-ready") return null;
134
144
  // Older files omit template — treat as standard so reuse stays usable.
135
145
  const templateRaw = o.template === undefined || o.template === null ? "standard" : o.template;
136
146
  if (typeof templateRaw !== "string" || !isTemplateId(templateRaw)) return null;
@@ -190,6 +200,19 @@ export function parseCreateDefaults(raw: unknown): CreateDefaults | null {
190
200
  return null;
191
201
  }
192
202
  if (typeof o.updatedAt !== "string") return null;
203
+ let locales: readonly string[] = [];
204
+ if (o.locales !== undefined && o.locales !== null) {
205
+ if (!Array.isArray(o.locales) || !o.locales.every((x) => typeof x === "string")) {
206
+ return null;
207
+ }
208
+ locales = o.locales.filter((tag) => tag !== "en");
209
+ }
210
+ // Older files omit pgdog — treat as off (opt-in).
211
+ let pgdog = false;
212
+ if (o.pgdog !== undefined && o.pgdog !== null) {
213
+ if (typeof o.pgdog !== "boolean") return null;
214
+ pgdog = o.pgdog;
215
+ }
193
216
  return {
194
217
  version: 1,
195
218
  template,
@@ -212,6 +235,8 @@ export function parseCreateDefaults(raw: unknown): CreateDefaults | null {
212
235
  ...(baseUrl !== undefined ? { baseUrl } : {}),
213
236
  ...(apiKeyEnv !== undefined ? { apiKeyEnv } : {}),
214
237
  },
238
+ locales,
239
+ pgdog,
215
240
  updatedAt: o.updatedAt,
216
241
  };
217
242
  }
@@ -234,13 +259,8 @@ function optionalStringOrNull(value: unknown): string | null | undefined | false
234
259
  function parsePins(raw: unknown): EnvDriverPins | null {
235
260
  if (!raw || typeof raw !== "object") return null;
236
261
  const o = raw as Record<string, unknown>;
237
- if (
238
- typeof o.local !== "string" ||
239
- typeof o.docker !== "string" ||
240
- typeof o.test !== "string" ||
241
- typeof o.prod !== "string"
242
- ) {
262
+ if (typeof o.dev !== "string" || typeof o.test !== "string" || typeof o.prod !== "string") {
243
263
  return null;
244
264
  }
245
- return { local: o.local, docker: o.docker, test: o.test, prod: o.prod };
265
+ return { dev: o.dev, test: o.test, prod: o.prod };
246
266
  }