create-oke 0.11.1 → 0.12.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 (61) hide show
  1. package/package.json +1 -1
  2. package/src/agents-md.ts +4 -1
  3. package/src/ai-setup/apply.ts +64 -6
  4. package/src/ai-setup/from-pref.ts +4 -4
  5. package/src/ai-setup/prompts.ts +1 -1
  6. package/src/cli.test.ts +133 -11
  7. package/src/cli.ts +180 -29
  8. package/src/create-defaults.test.ts +9 -0
  9. package/src/create-defaults.ts +27 -0
  10. package/src/customize-flow.test.ts +14 -7
  11. package/src/customize-flow.ts +2 -1
  12. package/src/drivers-catalog.ts +18 -9
  13. package/src/local-okengine.test.ts +2 -0
  14. package/src/local-okengine.ts +3 -0
  15. package/src/scaffold.ts +17 -5
  16. package/src/transform.test.ts +6 -3
  17. package/src/transform.ts +24 -1
  18. package/templates/advanced/.env.example +13 -5
  19. package/templates/advanced/README.md +3 -0
  20. package/templates/advanced/oke.config.ts +6 -5
  21. package/templates/advanced/package.json +14 -3
  22. package/templates/advanced/src/app.ts +2 -1
  23. package/templates/advanced/src/core.ts +12 -0
  24. package/templates/advanced/src/db/seed/index.ts +3 -1
  25. package/templates/advanced/src/flows/main/index.ts +4 -4
  26. package/templates/advanced/src/flows/notes/index.ts +73 -26
  27. package/templates/advanced/src/flows/notes/shapes.ts +9 -4
  28. package/templates/advanced/tests/advanced.test.ts +3 -2
  29. package/templates/advanced/web/index.html +12 -0
  30. package/templates/advanced/web/src/App.css +78 -0
  31. package/templates/advanced/web/src/App.tsx +100 -0
  32. package/templates/advanced/web/src/client.ts +38 -0
  33. package/templates/advanced/web/src/index.css +18 -0
  34. package/templates/advanced/web/src/main.tsx +13 -0
  35. package/templates/advanced/web/src/vite-env.d.ts +17 -0
  36. package/templates/advanced/web/tsconfig.app.json +24 -0
  37. package/templates/advanced/web/tsconfig.json +4 -0
  38. package/templates/advanced/web/tsconfig.node.json +20 -0
  39. package/templates/advanced/web/vite.config.ts +32 -0
  40. package/templates/standard/.env.example +13 -5
  41. package/templates/standard/README.md +3 -0
  42. package/templates/standard/oke.config.ts +4 -3
  43. package/templates/standard/package.json +14 -3
  44. package/templates/standard/src/app.ts +2 -1
  45. package/templates/standard/src/core.ts +12 -0
  46. package/templates/standard/src/db/seed/index.ts +2 -0
  47. package/templates/standard/src/flows/main/index.ts +4 -4
  48. package/templates/standard/src/flows/notes/index.ts +12 -12
  49. package/templates/standard/src/flows/notes/shapes.ts +2 -3
  50. package/templates/standard/tests/standard.test.ts +3 -2
  51. package/templates/standard/web/index.html +12 -0
  52. package/templates/standard/web/src/App.css +78 -0
  53. package/templates/standard/web/src/App.tsx +100 -0
  54. package/templates/standard/web/src/client.ts +38 -0
  55. package/templates/standard/web/src/index.css +18 -0
  56. package/templates/standard/web/src/main.tsx +13 -0
  57. package/templates/standard/web/src/vite-env.d.ts +17 -0
  58. package/templates/standard/web/tsconfig.app.json +24 -0
  59. package/templates/standard/web/tsconfig.json +4 -0
  60. package/templates/standard/web/tsconfig.node.json +20 -0
  61. package/templates/standard/web/vite.config.ts +32 -0
package/src/cli.ts CHANGED
@@ -25,10 +25,13 @@ import { existsSync, rmSync } from "node:fs";
25
25
  import { agentsMdContent } from "./agents-md.ts";
26
26
  import {
27
27
  createDefaultsPath,
28
+ isCreateProxyId,
28
29
  readCreateDefaults,
29
30
  writeCreateDefaults,
30
31
  type CreateDefaults,
32
+ type CreateProxyId,
31
33
  type EnvDriverPins,
34
+ CREATE_PROXY_IDS,
32
35
  } from "./create-defaults.ts";
33
36
  import { docsUrl } from "./docs-origin.ts";
34
37
  import { applyAiSetup, type AiSetupApplyInput } from "./ai-setup/apply.ts";
@@ -39,6 +42,7 @@ import {
39
42
  } from "./ai-setup/from-pref.ts";
40
43
  import { askAiSetup } from "./ai-setup/prompts.ts";
41
44
  import { askCustomizeFlow } from "./customize-flow.ts";
45
+ import { recommendedDefaults } from "./drivers-catalog.ts";
42
46
  import { parseExtraLocales } from "./locales.ts";
43
47
  import {
44
48
  scaffold,
@@ -89,6 +93,11 @@ export type CliArgs = {
89
93
  * `undefined` = ask (interactive) / off (`--yes`).
90
94
  */
91
95
  readonly pgdog: boolean | undefined;
96
+ /**
97
+ * Proxy pin from CLI.
98
+ * `undefined` = ask (interactive) / none (`--yes`).
99
+ */
100
+ readonly proxy: CreateProxyId | undefined;
92
101
  readonly targetDir: string | undefined;
93
102
  };
94
103
 
@@ -152,6 +161,8 @@ export type InteractiveAnswers = {
152
161
  readonly locales: readonly string[];
153
162
  /** Pin PgDog in front of Postgres. */
154
163
  readonly pgdog: boolean;
164
+ /** Pin `images.proxy` (`none` = unset). */
165
+ readonly proxy: CreateProxyId;
155
166
  };
156
167
 
157
168
  /**
@@ -170,6 +181,8 @@ export type ScaffoldCallArgs = {
170
181
  readonly locales?: readonly string[];
171
182
  /** Pin PgDog in front of Postgres. */
172
183
  readonly pgdog?: boolean;
184
+ /** Pin `images.proxy`. */
185
+ readonly proxy?: CreateProxyId;
173
186
  };
174
187
 
175
188
  /**
@@ -190,6 +203,7 @@ export function parseArgs(argv: readonly string[]): CliArgs {
190
203
  let ai: AiCliMode = "prompt";
191
204
  let locales: readonly string[] = [];
192
205
  let pgdog: boolean | undefined;
206
+ let proxy: CreateProxyId | undefined;
193
207
  let targetDir: string | undefined;
194
208
 
195
209
  for (let i = 0; i < argv.length; i++) {
@@ -301,6 +315,26 @@ export function parseArgs(argv: readonly string[]): CliArgs {
301
315
  pgdog = false;
302
316
  continue;
303
317
  }
318
+ if (a === "--proxy") {
319
+ const next = argv[++i];
320
+ if (!next || !isCreateProxyId(next)) {
321
+ throw new Error(`create-oke: --proxy must be one of ${CREATE_PROXY_IDS.join("|")}`);
322
+ }
323
+ proxy = next;
324
+ continue;
325
+ }
326
+ if (a.startsWith("--proxy=")) {
327
+ const value = a.slice("--proxy=".length);
328
+ if (!isCreateProxyId(value)) {
329
+ throw new Error(`create-oke: --proxy must be one of ${CREATE_PROXY_IDS.join("|")}`);
330
+ }
331
+ proxy = value;
332
+ continue;
333
+ }
334
+ if (a === "--no-proxy") {
335
+ proxy = "none";
336
+ continue;
337
+ }
304
338
  if (a.startsWith("-")) {
305
339
  throw new Error(`create-oke: unknown option ${a}`);
306
340
  }
@@ -317,6 +351,11 @@ export function parseArgs(argv: readonly string[]): CliArgs {
317
351
  if (argv.includes("--pgdog") && argv.includes("--no-pgdog")) {
318
352
  throw new Error("create-oke: use either --pgdog or --no-pgdog, not both");
319
353
  }
354
+ if (argv.includes("--proxy") || argv.some((x) => x.startsWith("--proxy="))) {
355
+ if (argv.includes("--no-proxy")) {
356
+ throw new Error("create-oke: use either --proxy or --no-proxy, not both");
357
+ }
358
+ }
320
359
 
321
360
  if (name !== undefined) {
322
361
  targetDir = resolve(name);
@@ -335,6 +374,7 @@ export function parseArgs(argv: readonly string[]): CliArgs {
335
374
  ai,
336
375
  locales,
337
376
  pgdog,
377
+ proxy,
338
378
  targetDir,
339
379
  };
340
380
  }
@@ -353,9 +393,9 @@ export function formatCdPath(targetDir: string): string {
353
393
  /**
354
394
  * Post-scaffold next-steps block — shared by interactive and flag-driven paths.
355
395
  *
356
- * @param result - Successful scaffold result
396
+ * @param result - Successful scaffold result (only `label` + `targetDir` are read)
357
397
  */
358
- export function nextStepsText(result: ScaffoldResult): string {
398
+ export function nextStepsText(result: Pick<ScaffoldResult, "label" | "targetDir">): string {
359
399
  return `
360
400
  Scaffolded ${result.label} → ${result.targetDir}
361
401
 
@@ -365,6 +405,7 @@ Next steps:
365
405
  bun install
366
406
  oke schema generate # system stubs → .oke/schema/oke.ts (also runs on db push)
367
407
  oke dev # app :6530 · Console :6533 · MCP :6535
408
+ bun run web # Vite SPA — proxies /notes · /health to the app
368
409
 
369
410
  Docs: ${docsUrl("/docs")}
370
411
  `;
@@ -399,6 +440,8 @@ Options:
399
440
  --locales <tags> Extra languages beyond English (e.g. ar or ar,fr)
400
441
  --pgdog Pin PgDog pooling in front of Postgres
401
442
  --no-pgdog Skip PgDog (default for --yes)
443
+ --proxy <id> Pin reverse proxy: ${CREATE_PROXY_IDS.join("|")}
444
+ --no-proxy Skip proxy (default for --yes)
402
445
  -h, --help Show this help
403
446
 
404
447
  Template:
@@ -406,9 +449,10 @@ ${templateLines}
406
449
 
407
450
  On a TTY: pick standard|advanced, then recommended defaults, customize
408
451
  (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.
452
+ Customize / Off), optional locales + PgDog + proxy, or reuse when
453
+ saved for that template. Reuse applies saved locales / PgDog / proxy
454
+ without re-asking. Answers write to ~/.oke/create-defaults.json.
455
+ Non-TTY / --yes stay English-only / no PgDog / no proxy unless flagged.
412
456
  `;
413
457
  }
414
458
 
@@ -453,6 +497,7 @@ export function scaffoldArgsFromCli(args: CliArgs): ScaffoldCallArgs {
453
497
  aiApply: args.ai === "force" ? nonInteractiveAiApply("llama-cpp") : null,
454
498
  locales: args.locales,
455
499
  pgdog: args.pgdog ?? false,
500
+ proxy: args.proxy ?? "none",
456
501
  };
457
502
  }
458
503
 
@@ -503,6 +548,7 @@ export function scaffoldArgsFromAnswers(
503
548
  aiApply: answers.aiApply,
504
549
  locales: answers.locales,
505
550
  pgdog: answers.pgdog,
551
+ proxy: answers.proxy,
506
552
  };
507
553
  }
508
554
 
@@ -519,6 +565,7 @@ export type AskInteractiveFn = (partial: {
519
565
  readonly template?: TemplateId;
520
566
  readonly locales?: readonly string[];
521
567
  readonly pgdog?: boolean;
568
+ readonly proxy?: CreateProxyId;
522
569
  }) => Promise<InteractiveAnswers | null>;
523
570
 
524
571
  /** Injectable persistence seams for tests. */
@@ -528,6 +575,52 @@ export type CreateDefaultsIo = {
528
575
  readonly write: (defaults: CreateDefaults) => void;
529
576
  };
530
577
 
578
+ /**
579
+ * Merge wizard locales / PgDog / proxy into a create-defaults document to persist.
580
+ *
581
+ * Prefer the in-session answers (customize / reuse), else same-template
582
+ * previous settings, else recommended pins for the template.
583
+ *
584
+ * @param input - Template, locales, pgdog, proxy, and optional session / previous docs
585
+ */
586
+ export function withWizardExtras(input: {
587
+ readonly template: TemplateId;
588
+ readonly locales: readonly string[];
589
+ readonly pgdog: boolean;
590
+ readonly proxy: CreateProxyId;
591
+ readonly session: CreateDefaults | undefined;
592
+ readonly previous: CreateDefaults | null;
593
+ }): CreateDefaults {
594
+ const { template, locales, pgdog, proxy, session, previous } = input;
595
+ const base =
596
+ session ??
597
+ (previous?.template === template ? previous : null) ??
598
+ recommendedDefaults("docker-ready", template);
599
+ return {
600
+ ...base,
601
+ template,
602
+ locales,
603
+ pgdog,
604
+ proxy,
605
+ updatedAt: new Date().toISOString(),
606
+ };
607
+ }
608
+
609
+ /** @deprecated Prefer {@link withWizardExtras}. */
610
+ export function withLocalesPgDog(input: {
611
+ readonly template: TemplateId;
612
+ readonly locales: readonly string[];
613
+ readonly pgdog: boolean;
614
+ readonly proxy?: CreateProxyId;
615
+ readonly session: CreateDefaults | undefined;
616
+ readonly previous: CreateDefaults | null;
617
+ }): CreateDefaults {
618
+ return withWizardExtras({
619
+ ...input,
620
+ proxy: input.proxy ?? input.session?.proxy ?? input.previous?.proxy ?? "none",
621
+ });
622
+ }
623
+
531
624
  /**
532
625
  * Collect interactive answers via `@clack/prompts`.
533
626
  *
@@ -544,6 +637,7 @@ export async function askInteractiveAnswers(
544
637
  readonly template?: TemplateId;
545
638
  readonly locales?: readonly string[];
546
639
  readonly pgdog?: boolean;
640
+ readonly proxy?: CreateProxyId;
547
641
  } = {},
548
642
  io: CreateDefaultsIo = {
549
643
  path: createDefaultsPath(),
@@ -582,6 +676,7 @@ export async function askInteractiveAnswers(
582
676
  let createDefaults: CreateDefaults | undefined;
583
677
  let aiApply: AiSetupApplyInput | null = null;
584
678
  let persistDefaults = false;
679
+ let reused = false;
585
680
 
586
681
  for (;;) {
587
682
  const previous = io.read();
@@ -608,6 +703,7 @@ export async function askInteractiveAnswers(
608
703
  continue;
609
704
  }
610
705
  createDefaults = prev;
706
+ reused = true;
611
707
  if (prev.ai.enabled && partial.ai !== "skip") {
612
708
  aiApply = applyInputFromAiPref(prev.ai);
613
709
  if (!aiApply?.chatModel && prev.ai.provider && prev.ai.provider !== "mock") {
@@ -644,36 +740,66 @@ export async function askInteractiveAnswers(
644
740
  aiApply = picked;
645
741
  }
646
742
 
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;
743
+ // Reuse applies saved locales / PgDog / proxy without re-asking (CLI flags still win).
744
+ let locales: readonly string[];
745
+ let pgdog: boolean;
746
+ let proxy: CreateProxyId;
747
+
748
+ if (reused && createDefaults) {
749
+ locales =
750
+ partial.locales !== undefined && partial.locales.length > 0
751
+ ? partial.locales
752
+ : createDefaults.locales;
753
+ pgdog = partial.pgdog !== undefined ? partial.pgdog : createDefaults.pgdog;
754
+ proxy = partial.proxy !== undefined ? partial.proxy : createDefaults.proxy;
755
+ } else {
756
+ if (partial.locales !== undefined && partial.locales.length > 0) {
757
+ locales = partial.locales;
758
+ } else {
759
+ const pickedLocales = await askExtraLocales(createDefaults?.locales ?? []);
760
+ if (pickedLocales === null) return null;
761
+ locales = pickedLocales;
762
+ }
763
+
764
+ const usesPostgres = createDefaults
765
+ ? createDefaults.drivers.store.sql.dev === "postgres" ||
766
+ createDefaults.drivers.store.sql.prod === "postgres" ||
767
+ createDefaults.drivers.store.sql.dev === "pgvector" ||
768
+ createDefaults.drivers.store.sql.prod === "pgvector"
769
+ : true;
770
+ pgdog = false;
771
+ if (usesPostgres) {
772
+ if (partial.pgdog !== undefined) {
773
+ pgdog = partial.pgdog;
774
+ } else {
775
+ const picked = await askPgDog(createDefaults?.pgdog ?? false);
776
+ if (picked === null) return null;
777
+ pgdog = picked;
778
+ }
779
+ }
780
+
781
+ if (partial.proxy !== undefined) {
782
+ proxy = partial.proxy;
660
783
  } else {
661
- const picked = await askPgDog(createDefaults?.pgdog ?? false);
784
+ const picked = await askProxy(createDefaults?.proxy ?? "none");
662
785
  if (picked === null) return null;
663
- pgdog = picked;
786
+ proxy = picked;
664
787
  }
665
788
  }
666
789
 
790
+ const persisted = withWizardExtras({
791
+ template,
792
+ locales,
793
+ pgdog,
794
+ proxy,
795
+ session: createDefaults,
796
+ previous: io.read(),
797
+ });
667
798
  if (createDefaults) {
668
- createDefaults = {
669
- ...createDefaults,
670
- locales,
671
- pgdog,
672
- updatedAt: new Date().toISOString(),
673
- };
799
+ createDefaults = persisted;
674
800
  }
675
- if (persistDefaults && createDefaults) {
676
- io.write(createDefaults);
801
+ io.write(persisted);
802
+ if (persistDefaults) {
677
803
  note(`Saved globally for next projects → ${io.path}`, "Defaults");
678
804
  }
679
805
 
@@ -694,6 +820,7 @@ export async function askInteractiveAnswers(
694
820
  aiApply,
695
821
  locales,
696
822
  pgdog,
823
+ proxy,
697
824
  };
698
825
  }
699
826
 
@@ -744,6 +871,27 @@ async function askPgDog(initial: boolean = false): Promise<boolean | null> {
744
871
  return Boolean(value);
745
872
  }
746
873
 
874
+ /**
875
+ * Ask which reverse proxy to pin (or none).
876
+ *
877
+ * @param initial - Prefill from reused create-defaults
878
+ * @returns Proxy id, or `null` on cancel
879
+ */
880
+ async function askProxy(initial: CreateProxyId = "none"): Promise<CreateProxyId | null> {
881
+ const value = await select({
882
+ message: "Add a reverse proxy in front of the app?",
883
+ options: [
884
+ { value: "none", label: "No", hint: "app publishes :6530" },
885
+ { value: "caddy", label: "Caddy", hint: "automatic HTTPS · Caddyfile" },
886
+ { value: "traefik", label: "Traefik", hint: "Docker labels · --scale app=N" },
887
+ { value: "nginx", label: "nginx", hint: "static nginx.conf reverse proxy" },
888
+ ],
889
+ initialValue: initial,
890
+ });
891
+ if (isCancel(value)) return null;
892
+ return value as CreateProxyId;
893
+ }
894
+
747
895
  /**
748
896
  * Provider passed to `oke ai setup` — prefer native Ollama when either env uses it.
749
897
  *
@@ -871,8 +1019,9 @@ async function runInteractive(
871
1019
  agentsMd: args.agentsMd,
872
1020
  sqlDriver: args.sqlDriverExplicit ? args.sqlDriver : undefined,
873
1021
  ai: args.ai,
874
- locales: args.locales,
1022
+ ...(args.locales.length > 0 ? { locales: args.locales } : {}),
875
1023
  ...(args.pgdog !== undefined ? { pgdog: args.pgdog } : {}),
1024
+ ...(args.proxy !== undefined ? { proxy: args.proxy } : {}),
876
1025
  });
877
1026
  if (answers === null) {
878
1027
  cancel("Cancelled.");
@@ -911,6 +1060,7 @@ async function runScaffold(
911
1060
  aiApply,
912
1061
  locales,
913
1062
  pgdog,
1063
+ proxy,
914
1064
  interactive,
915
1065
  install,
916
1066
  startDev,
@@ -947,6 +1097,7 @@ async function runScaffold(
947
1097
  ...(createDefaults !== undefined ? { createDefaults } : {}),
948
1098
  ...(locales !== undefined ? { locales } : {}),
949
1099
  ...(pgdog !== undefined ? { pgdog } : {}),
1100
+ ...(proxy !== undefined ? { proxy } : {}),
950
1101
  });
951
1102
  if (spun) spun.stop("Scaffolded.");
952
1103
 
@@ -45,6 +45,13 @@ describe("parseCreateDefaults", () => {
45
45
  const parsed = parseCreateDefaults(rest);
46
46
  expect(parsed?.template).toBe("standard");
47
47
  });
48
+
49
+ test("older files without proxy default to none", () => {
50
+ const d = recommendedDefaults("docker-ready", "standard");
51
+ const { proxy: _p, ...rest } = d;
52
+ const parsed = parseCreateDefaults(rest);
53
+ expect(parsed?.proxy).toBe("none");
54
+ });
48
55
  });
49
56
 
50
57
  describe("read/write round-trip", () => {
@@ -71,6 +78,7 @@ describe("read/write round-trip", () => {
71
78
  ai: { enabled: true, provider: "ollama", driver: "ollama" },
72
79
  locales: ["ar"],
73
80
  pgdog: true,
81
+ proxy: "caddy",
74
82
  });
75
83
  writeCreateDefaults(original, path);
76
84
  const loaded = readCreateDefaults(path);
@@ -82,6 +90,7 @@ describe("read/write round-trip", () => {
82
90
  expect(loaded!.profile).toBe("docker-ready");
83
91
  expect(loaded!.locales).toEqual(["ar"]);
84
92
  expect(loaded!.pgdog).toBe(true);
93
+ expect(loaded!.proxy).toBe("caddy");
85
94
  } finally {
86
95
  rmSync(home, { recursive: true, force: true });
87
96
  }
@@ -13,6 +13,9 @@ import { isTemplateId, type TemplateId } from "./templates.ts";
13
13
  /** Create profile — Docker Compose is always the `dev` runtime. */
14
14
  export type CreateProfile = "docker-ready";
15
15
 
16
+ /** Opt-in reverse proxy pinned into `images.proxy`. */
17
+ export type CreateProxyId = "none" | "caddy" | "traefik" | "nginx";
18
+
16
19
  /** Env columns written into `oke.config.ts` driver maps. */
17
20
  export type EnvDriverPins = {
18
21
  readonly dev: string;
@@ -71,12 +74,20 @@ export type CreateDefaults = {
71
74
  * Default false — opt in via the wizard or `--pgdog`.
72
75
  */
73
76
  readonly pgdog: boolean;
77
+ /**
78
+ * Pin `images.proxy` (Caddy / Traefik / nginx). `none` leaves the role unset.
79
+ * Default `none` — opt in via the wizard or `--proxy`.
80
+ */
81
+ readonly proxy: CreateProxyId;
74
82
  readonly updatedAt: string;
75
83
  };
76
84
 
77
85
  /** Relative path under the user home directory. */
78
86
  export const CREATE_DEFAULTS_RELATIVE = ".oke/create-defaults.json";
79
87
 
88
+ /** Valid proxy wizard / CLI values. */
89
+ export const CREATE_PROXY_IDS = ["none", "caddy", "traefik", "nginx"] as const;
90
+
80
91
  /**
81
92
  * Absolute path to the create-defaults file.
82
93
  *
@@ -130,6 +141,15 @@ export function toCreateDefaults(
130
141
  };
131
142
  }
132
143
 
144
+ /**
145
+ * Whether `value` is a known {@link CreateProxyId}.
146
+ *
147
+ * @param value - Candidate
148
+ */
149
+ export function isCreateProxyId(value: string): value is CreateProxyId {
150
+ return (CREATE_PROXY_IDS as readonly string[]).includes(value);
151
+ }
152
+
133
153
  /**
134
154
  * Validate a parsed JSON value as {@link CreateDefaults}.
135
155
  *
@@ -213,6 +233,12 @@ export function parseCreateDefaults(raw: unknown): CreateDefaults | null {
213
233
  if (typeof o.pgdog !== "boolean") return null;
214
234
  pgdog = o.pgdog;
215
235
  }
236
+ // Older files omit proxy — treat as none (opt-in).
237
+ let proxy: CreateProxyId = "none";
238
+ if (o.proxy !== undefined && o.proxy !== null) {
239
+ if (typeof o.proxy !== "string" || !isCreateProxyId(o.proxy)) return null;
240
+ proxy = o.proxy;
241
+ }
216
242
  return {
217
243
  version: 1,
218
244
  template,
@@ -237,6 +263,7 @@ export function parseCreateDefaults(raw: unknown): CreateDefaults | null {
237
263
  },
238
264
  locales,
239
265
  pgdog,
266
+ proxy,
240
267
  updatedAt: o.updatedAt,
241
268
  };
242
269
  }
@@ -48,13 +48,20 @@ describe("catalog labels", () => {
48
48
 
49
49
  describe("recommendedAiApply", () => {
50
50
  test("returns llama.cpp (openai-compatible) with curated ai/ model", () => {
51
- const apply = recommendedAiApply();
52
- expect(apply.driver).toBe("openai-compatible");
53
- expect(apply.chatModel).toBe("granite3.3:2b");
54
- expect(apply.baseUrl).toContain("8080");
55
- expect(apply.image).toContain("llama.cpp");
56
- expect(apply.image).not.toContain("latest");
57
- expect(apply.visionModel).toBeNull();
51
+ const prev = process.env.OKE_AI_URL;
52
+ delete process.env.OKE_AI_URL;
53
+ try {
54
+ const apply = recommendedAiApply();
55
+ expect(apply.driver).toBe("openai-compatible");
56
+ expect(apply.chatModel).toBe("granite3.3:2b");
57
+ expect(apply.baseUrl).toContain("8080");
58
+ expect(apply.image).toContain("llama.cpp");
59
+ expect(apply.image).not.toContain("latest");
60
+ expect(apply.visionModel).toBeNull();
61
+ } finally {
62
+ if (prev !== undefined) process.env.OKE_AI_URL = prev;
63
+ else delete process.env.OKE_AI_URL;
64
+ }
58
65
  });
59
66
  });
60
67
 
@@ -240,9 +240,10 @@ export async function askCustomizeFlow(
240
240
  profile: "docker-ready",
241
241
  drivers: { ...drivers, ai: aiPins },
242
242
  ai: aiPref,
243
- // Locales / PgDog are asked once in the main wizard (not per customize pass).
243
+ // Locales / PgDog / proxy are asked once in the main wizard (not per customize pass).
244
244
  locales: [],
245
245
  pgdog: false,
246
+ proxy: "none",
246
247
  });
247
248
  }
248
249
 
@@ -89,7 +89,7 @@ export const JOURNAL_CHOICES: readonly DriverChoice[] = [
89
89
  * Vault backends. `vault` is okengine's own encrypted-at-rest store: it lives
90
90
  * in the app's Postgres, so it needs no extra container, and it is the only
91
91
  * backend with `oke vault init` / `seal` / `audit`. `managed` reads from a
92
- * provider secret store (e.g. AWS Secrets Manager).
92
+ * provider secret store (AWS, Azure, GCP, Doppler, 1Password).
93
93
  */
94
94
  export const VAULT_CHOICES: readonly DriverChoice[] = [
95
95
  { value: "env", label: "env (dotenv layers only)" },
@@ -112,13 +112,13 @@ export const EMAIL_CHOICES: readonly DriverChoice[] = [
112
112
  * Pinned local-inference images — must match `src/docker/recipes/*` pins.
113
113
  * Never `latest` (GGUF-parser CVE floors: llama.cpp ≥ b8146, Ollama ≥ 0.17.1).
114
114
  */
115
- export const LLAMA_CPP_IMAGE = "ghcr.io/ggml-org/llama.cpp:server-b10290";
115
+ export const LLAMA_CPP_IMAGE = "ghcr.io/ggml-org/llama.cpp:server-b10450";
116
116
  /** @see LLAMA_CPP_IMAGE */
117
- export const OLLAMA_IMAGE = "ollama/ollama:0.32.6";
117
+ export const OLLAMA_IMAGE = "ollama/ollama:0.32.13";
118
118
  /** @see LLAMA_CPP_IMAGE */
119
- export const VLLM_IMAGE = "vllm/vllm-openai:v0.26.0";
119
+ export const VLLM_IMAGE = "vllm/vllm-openai:v0.27.1";
120
120
  /** @see LLAMA_CPP_IMAGE */
121
- export const SGLANG_IMAGE = "lmsysorg/sglang:v0.5.16-runtime";
121
+ export const SGLANG_IMAGE = "lmsysorg/sglang:v0.5.17-runtime";
122
122
 
123
123
  /** AI menu providers → protocol driver. */
124
124
  export const AI_PROVIDERS = [
@@ -191,6 +191,7 @@ export function recommendedDefaults(
191
191
  ai: { enabled: false, provider: null, driver: null },
192
192
  locales: [],
193
193
  pgdog: false,
194
+ proxy: "none",
194
195
  updatedAt: new Date().toISOString(),
195
196
  };
196
197
  }
@@ -229,10 +230,18 @@ export function aiDriverForProvider(provider: string): string {
229
230
  /** Default image pins keyed by role (standard template). */
230
231
  export const DEFAULT_IMAGES: Readonly<Record<string, string>> = {
231
232
  "store.sql": "postgres:18-alpine",
232
- pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.51",
233
+ pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.53",
233
234
  "store.kv": "redis:8-alpine",
234
- "store.files": "rustfs/rustfs:1.0.0-beta.11",
235
- "channel.email": "axllent/mailpit:v1.22.3",
236
- "store.index": "getmeili/meilisearch:v1.37",
235
+ "store.files": "rustfs/rustfs:1.0.0-rc.2",
236
+ "channel.email": "axllent/mailpit:v1.30.7",
237
+ "store.index": "getmeili/meilisearch:v1.53",
237
238
  ai: LLAMA_CPP_IMAGE,
239
+ proxy: "caddy:2-alpine",
240
+ };
241
+
242
+ /** Default `images.proxy` pins by wizard id (excluding `none`). */
243
+ export const PROXY_IMAGES: Readonly<Record<"caddy" | "traefik" | "nginx", string>> = {
244
+ caddy: "caddy:2-alpine",
245
+ traefik: "traefik:v3.7",
246
+ nginx: "nginx:1.31-alpine",
238
247
  };
@@ -29,6 +29,7 @@ describe("materializeLocalOkengineDependency", () => {
29
29
  workspaces?: unknown;
30
30
  peerDependencies?: Record<string, string>;
31
31
  exports?: Record<string, string>;
32
+ trustedDependencies?: readonly string[];
32
33
  };
33
34
  expect(pkg.name).toBe("okengine");
34
35
  expect(pkg.devDependencies).toBeUndefined();
@@ -40,6 +41,7 @@ describe("materializeLocalOkengineDependency", () => {
40
41
  expect(pkg.dependencies?.["drizzle-kit"]).toBe("1.0.0-rc.4");
41
42
  expect(pkg.dependencies?.["zod"]).toBeDefined();
42
43
  expect(pkg.exports?.["./config"]).toBe("./src/config/index.ts");
44
+ expect(pkg.trustedDependencies).toContain("@duckdb/node-api");
43
45
 
44
46
  // Real copies — Bun file: install drops directory symlinks.
45
47
  const src = join(stage, "src");
@@ -33,6 +33,7 @@ type ConsumerOkenginePackageJson = {
33
33
  readonly peerDependencies?: Record<string, string>;
34
34
  readonly peerDependenciesMeta?: Record<string, unknown>;
35
35
  readonly engines?: unknown;
36
+ readonly trustedDependencies?: readonly string[];
36
37
  };
37
38
 
38
39
  /**
@@ -92,6 +93,8 @@ export function materializeLocalOkengineDependency(localOkengineRoot: string): s
92
93
  ...(raw["exports"] !== undefined ? { exports: raw["exports"] } : {}),
93
94
  ...(Object.keys(stageDependencies).length > 0 ? { dependencies: stageDependencies } : {}),
94
95
  ...(raw["engines"] !== undefined ? { engines: raw["engines"] } : {}),
96
+ // Bun blocks native postinstalls unless listed — DuckDB runs queries need it.
97
+ trustedDependencies: ["@duckdb/node-api"],
95
98
  };
96
99
 
97
100
  const stage = localOkengineStageDir();