create-oke 0.11.2 → 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 (59) hide show
  1. package/package.json +1 -1
  2. package/src/agents-md.ts +4 -1
  3. package/src/ai-setup/apply.ts +3 -5
  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 +86 -14
  7. package/src/cli.ts +145 -29
  8. package/src/create-defaults.test.ts +9 -0
  9. package/src/create-defaults.ts +27 -0
  10. package/src/customize-flow.ts +2 -1
  11. package/src/drivers-catalog.ts +18 -9
  12. package/src/local-okengine.test.ts +2 -0
  13. package/src/local-okengine.ts +3 -0
  14. package/src/scaffold.ts +17 -5
  15. package/src/transform.test.ts +6 -3
  16. package/src/transform.ts +24 -1
  17. package/templates/advanced/.env.example +13 -5
  18. package/templates/advanced/README.md +3 -0
  19. package/templates/advanced/oke.config.ts +6 -5
  20. package/templates/advanced/package.json +14 -3
  21. package/templates/advanced/src/app.ts +2 -1
  22. package/templates/advanced/src/core.ts +12 -0
  23. package/templates/advanced/src/db/seed/index.ts +2 -0
  24. package/templates/advanced/src/flows/main/index.ts +4 -4
  25. package/templates/advanced/src/flows/notes/index.ts +14 -14
  26. package/templates/advanced/src/flows/notes/shapes.ts +2 -3
  27. package/templates/advanced/web/index.html +12 -0
  28. package/templates/advanced/web/src/App.css +78 -0
  29. package/templates/advanced/web/src/App.tsx +100 -0
  30. package/templates/advanced/web/src/client.ts +38 -0
  31. package/templates/advanced/web/src/index.css +18 -0
  32. package/templates/advanced/web/src/main.tsx +13 -0
  33. package/templates/advanced/web/src/vite-env.d.ts +17 -0
  34. package/templates/advanced/web/tsconfig.app.json +24 -0
  35. package/templates/advanced/web/tsconfig.json +4 -0
  36. package/templates/advanced/web/tsconfig.node.json +20 -0
  37. package/templates/advanced/web/vite.config.ts +32 -0
  38. package/templates/standard/.env.example +13 -5
  39. package/templates/standard/README.md +3 -0
  40. package/templates/standard/oke.config.ts +4 -3
  41. package/templates/standard/package.json +14 -3
  42. package/templates/standard/src/app.ts +2 -1
  43. package/templates/standard/src/core.ts +12 -0
  44. package/templates/standard/src/db/seed/index.ts +2 -0
  45. package/templates/standard/src/flows/main/index.ts +4 -4
  46. package/templates/standard/src/flows/notes/index.ts +12 -12
  47. package/templates/standard/src/flows/notes/shapes.ts +2 -3
  48. package/templates/standard/tests/standard.test.ts +3 -2
  49. package/templates/standard/web/index.html +12 -0
  50. package/templates/standard/web/src/App.css +78 -0
  51. package/templates/standard/web/src/App.tsx +100 -0
  52. package/templates/standard/web/src/client.ts +38 -0
  53. package/templates/standard/web/src/index.css +18 -0
  54. package/templates/standard/web/src/main.tsx +13 -0
  55. package/templates/standard/web/src/vite-env.d.ts +17 -0
  56. package/templates/standard/web/tsconfig.app.json +24 -0
  57. package/templates/standard/web/tsconfig.json +4 -0
  58. package/templates/standard/web/tsconfig.node.json +20 -0
  59. 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";
@@ -90,6 +93,11 @@ export type CliArgs = {
90
93
  * `undefined` = ask (interactive) / off (`--yes`).
91
94
  */
92
95
  readonly pgdog: boolean | undefined;
96
+ /**
97
+ * Proxy pin from CLI.
98
+ * `undefined` = ask (interactive) / none (`--yes`).
99
+ */
100
+ readonly proxy: CreateProxyId | undefined;
93
101
  readonly targetDir: string | undefined;
94
102
  };
95
103
 
@@ -153,6 +161,8 @@ export type InteractiveAnswers = {
153
161
  readonly locales: readonly string[];
154
162
  /** Pin PgDog in front of Postgres. */
155
163
  readonly pgdog: boolean;
164
+ /** Pin `images.proxy` (`none` = unset). */
165
+ readonly proxy: CreateProxyId;
156
166
  };
157
167
 
158
168
  /**
@@ -171,6 +181,8 @@ export type ScaffoldCallArgs = {
171
181
  readonly locales?: readonly string[];
172
182
  /** Pin PgDog in front of Postgres. */
173
183
  readonly pgdog?: boolean;
184
+ /** Pin `images.proxy`. */
185
+ readonly proxy?: CreateProxyId;
174
186
  };
175
187
 
176
188
  /**
@@ -191,6 +203,7 @@ export function parseArgs(argv: readonly string[]): CliArgs {
191
203
  let ai: AiCliMode = "prompt";
192
204
  let locales: readonly string[] = [];
193
205
  let pgdog: boolean | undefined;
206
+ let proxy: CreateProxyId | undefined;
194
207
  let targetDir: string | undefined;
195
208
 
196
209
  for (let i = 0; i < argv.length; i++) {
@@ -302,6 +315,26 @@ export function parseArgs(argv: readonly string[]): CliArgs {
302
315
  pgdog = false;
303
316
  continue;
304
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
+ }
305
338
  if (a.startsWith("-")) {
306
339
  throw new Error(`create-oke: unknown option ${a}`);
307
340
  }
@@ -318,6 +351,11 @@ export function parseArgs(argv: readonly string[]): CliArgs {
318
351
  if (argv.includes("--pgdog") && argv.includes("--no-pgdog")) {
319
352
  throw new Error("create-oke: use either --pgdog or --no-pgdog, not both");
320
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
+ }
321
359
 
322
360
  if (name !== undefined) {
323
361
  targetDir = resolve(name);
@@ -336,6 +374,7 @@ export function parseArgs(argv: readonly string[]): CliArgs {
336
374
  ai,
337
375
  locales,
338
376
  pgdog,
377
+ proxy,
339
378
  targetDir,
340
379
  };
341
380
  }
@@ -354,9 +393,9 @@ export function formatCdPath(targetDir: string): string {
354
393
  /**
355
394
  * Post-scaffold next-steps block — shared by interactive and flag-driven paths.
356
395
  *
357
- * @param result - Successful scaffold result
396
+ * @param result - Successful scaffold result (only `label` + `targetDir` are read)
358
397
  */
359
- export function nextStepsText(result: ScaffoldResult): string {
398
+ export function nextStepsText(result: Pick<ScaffoldResult, "label" | "targetDir">): string {
360
399
  return `
361
400
  Scaffolded ${result.label} → ${result.targetDir}
362
401
 
@@ -366,6 +405,7 @@ Next steps:
366
405
  bun install
367
406
  oke schema generate # system stubs → .oke/schema/oke.ts (also runs on db push)
368
407
  oke dev # app :6530 · Console :6533 · MCP :6535
408
+ bun run web # Vite SPA — proxies /notes · /health to the app
369
409
 
370
410
  Docs: ${docsUrl("/docs")}
371
411
  `;
@@ -400,6 +440,8 @@ Options:
400
440
  --locales <tags> Extra languages beyond English (e.g. ar or ar,fr)
401
441
  --pgdog Pin PgDog pooling in front of Postgres
402
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)
403
445
  -h, --help Show this help
404
446
 
405
447
  Template:
@@ -407,10 +449,10 @@ ${templateLines}
407
449
 
408
450
  On a TTY: pick standard|advanced, then recommended defaults, customize
409
451
  (Docker-first facets; store.index with none; AI setup Recommended /
410
- Customize / Off), optional extra locales + PgDog pooling, or reuse when
411
- saved for that template. Locales + PgDog (and customize pins) write to
412
- ~/.oke/create-defaults.json on every TTY run.
413
- 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.
414
456
  `;
415
457
  }
416
458
 
@@ -455,6 +497,7 @@ export function scaffoldArgsFromCli(args: CliArgs): ScaffoldCallArgs {
455
497
  aiApply: args.ai === "force" ? nonInteractiveAiApply("llama-cpp") : null,
456
498
  locales: args.locales,
457
499
  pgdog: args.pgdog ?? false,
500
+ proxy: args.proxy ?? "none",
458
501
  };
459
502
  }
460
503
 
@@ -505,6 +548,7 @@ export function scaffoldArgsFromAnswers(
505
548
  aiApply: answers.aiApply,
506
549
  locales: answers.locales,
507
550
  pgdog: answers.pgdog,
551
+ proxy: answers.proxy,
508
552
  };
509
553
  }
510
554
 
@@ -521,6 +565,7 @@ export type AskInteractiveFn = (partial: {
521
565
  readonly template?: TemplateId;
522
566
  readonly locales?: readonly string[];
523
567
  readonly pgdog?: boolean;
568
+ readonly proxy?: CreateProxyId;
524
569
  }) => Promise<InteractiveAnswers | null>;
525
570
 
526
571
  /** Injectable persistence seams for tests. */
@@ -531,21 +576,22 @@ export type CreateDefaultsIo = {
531
576
  };
532
577
 
533
578
  /**
534
- * Merge wizard locales / PgDog into a create-defaults document to persist.
579
+ * Merge wizard locales / PgDog / proxy into a create-defaults document to persist.
535
580
  *
536
581
  * Prefer the in-session answers (customize / reuse), else same-template
537
582
  * previous settings, else recommended pins for the template.
538
583
  *
539
- * @param input - Template, locales, pgdog, and optional session / previous docs
584
+ * @param input - Template, locales, pgdog, proxy, and optional session / previous docs
540
585
  */
541
- export function withLocalesPgDog(input: {
586
+ export function withWizardExtras(input: {
542
587
  readonly template: TemplateId;
543
588
  readonly locales: readonly string[];
544
589
  readonly pgdog: boolean;
590
+ readonly proxy: CreateProxyId;
545
591
  readonly session: CreateDefaults | undefined;
546
592
  readonly previous: CreateDefaults | null;
547
593
  }): CreateDefaults {
548
- const { template, locales, pgdog, session, previous } = input;
594
+ const { template, locales, pgdog, proxy, session, previous } = input;
549
595
  const base =
550
596
  session ??
551
597
  (previous?.template === template ? previous : null) ??
@@ -555,10 +601,26 @@ export function withLocalesPgDog(input: {
555
601
  template,
556
602
  locales,
557
603
  pgdog,
604
+ proxy,
558
605
  updatedAt: new Date().toISOString(),
559
606
  };
560
607
  }
561
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
+
562
624
  /**
563
625
  * Collect interactive answers via `@clack/prompts`.
564
626
  *
@@ -575,6 +637,7 @@ export async function askInteractiveAnswers(
575
637
  readonly template?: TemplateId;
576
638
  readonly locales?: readonly string[];
577
639
  readonly pgdog?: boolean;
640
+ readonly proxy?: CreateProxyId;
578
641
  } = {},
579
642
  io: CreateDefaultsIo = {
580
643
  path: createDefaultsPath(),
@@ -613,6 +676,7 @@ export async function askInteractiveAnswers(
613
676
  let createDefaults: CreateDefaults | undefined;
614
677
  let aiApply: AiSetupApplyInput | null = null;
615
678
  let persistDefaults = false;
679
+ let reused = false;
616
680
 
617
681
  for (;;) {
618
682
  const previous = io.read();
@@ -639,6 +703,7 @@ export async function askInteractiveAnswers(
639
703
  continue;
640
704
  }
641
705
  createDefaults = prev;
706
+ reused = true;
642
707
  if (prev.ai.enabled && partial.ai !== "skip") {
643
708
  aiApply = applyInputFromAiPref(prev.ai);
644
709
  if (!aiApply?.chatModel && prev.ai.provider && prev.ai.provider !== "mock") {
@@ -675,32 +740,58 @@ export async function askInteractiveAnswers(
675
740
  aiApply = picked;
676
741
  }
677
742
 
678
- const locales = await askExtraLocales(partial.locales ?? createDefaults?.locales ?? []);
679
- if (locales === null) return null;
680
-
681
- const usesPostgres = createDefaults
682
- ? createDefaults.drivers.store.sql.dev === "postgres" ||
683
- createDefaults.drivers.store.sql.prod === "postgres" ||
684
- createDefaults.drivers.store.sql.dev === "pgvector" ||
685
- createDefaults.drivers.store.sql.prod === "pgvector"
686
- : true;
687
- let pgdog = false;
688
- if (usesPostgres) {
689
- if (partial.pgdog !== undefined) {
690
- 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;
691
783
  } else {
692
- const picked = await askPgDog(createDefaults?.pgdog ?? false);
784
+ const picked = await askProxy(createDefaults?.proxy ?? "none");
693
785
  if (picked === null) return null;
694
- pgdog = picked;
786
+ proxy = picked;
695
787
  }
696
788
  }
697
789
 
698
- // Locales / PgDog are asked on every TTY run — always persist them so reuse
699
- // (and recommended) keep the last answers in ~/.oke/create-defaults.json.
700
- const persisted = withLocalesPgDog({
790
+ const persisted = withWizardExtras({
701
791
  template,
702
792
  locales,
703
793
  pgdog,
794
+ proxy,
704
795
  session: createDefaults,
705
796
  previous: io.read(),
706
797
  });
@@ -729,6 +820,7 @@ export async function askInteractiveAnswers(
729
820
  aiApply,
730
821
  locales,
731
822
  pgdog,
823
+ proxy,
732
824
  };
733
825
  }
734
826
 
@@ -779,6 +871,27 @@ async function askPgDog(initial: boolean = false): Promise<boolean | null> {
779
871
  return Boolean(value);
780
872
  }
781
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
+
782
895
  /**
783
896
  * Provider passed to `oke ai setup` — prefer native Ollama when either env uses it.
784
897
  *
@@ -906,8 +1019,9 @@ async function runInteractive(
906
1019
  agentsMd: args.agentsMd,
907
1020
  sqlDriver: args.sqlDriverExplicit ? args.sqlDriver : undefined,
908
1021
  ai: args.ai,
909
- locales: args.locales,
1022
+ ...(args.locales.length > 0 ? { locales: args.locales } : {}),
910
1023
  ...(args.pgdog !== undefined ? { pgdog: args.pgdog } : {}),
1024
+ ...(args.proxy !== undefined ? { proxy: args.proxy } : {}),
911
1025
  });
912
1026
  if (answers === null) {
913
1027
  cancel("Cancelled.");
@@ -946,6 +1060,7 @@ async function runScaffold(
946
1060
  aiApply,
947
1061
  locales,
948
1062
  pgdog,
1063
+ proxy,
949
1064
  interactive,
950
1065
  install,
951
1066
  startDev,
@@ -982,6 +1097,7 @@ async function runScaffold(
982
1097
  ...(createDefaults !== undefined ? { createDefaults } : {}),
983
1098
  ...(locales !== undefined ? { locales } : {}),
984
1099
  ...(pgdog !== undefined ? { pgdog } : {}),
1100
+ ...(proxy !== undefined ? { proxy } : {}),
985
1101
  });
986
1102
  if (spun) spun.stop("Scaffolded.");
987
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
  }
@@ -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();
package/src/scaffold.ts CHANGED
@@ -16,13 +16,14 @@ import { basename, join, relative, resolve } from "node:path";
16
16
  import { pathToFileURL } from "node:url";
17
17
  import { resolveLocalOkengineRoot, resolveTemplateDir, type TemplateId } from "./templates.ts";
18
18
  import { agentsMdContent } from "./agents-md.ts";
19
- import type { CreateDefaults } from "./create-defaults.ts";
19
+ import type { CreateDefaults, CreateProxyId } from "./create-defaults.ts";
20
20
  import { DEFAULT_IMAGES, TEMPLATE_DEV } from "./drivers-catalog.ts";
21
21
  import { applyLocalesToProject } from "./locales.ts";
22
22
  import {
23
23
  DEFAULT_SQL_DRIVER,
24
24
  applyCreateAnswers,
25
25
  applyPgDogToConfig,
26
+ applyProxyToConfig,
26
27
  extractImages,
27
28
  sanitizeProjectName,
28
29
  shouldSkipTemplatePath,
@@ -64,6 +65,11 @@ export type ScaffoldOptions = {
64
65
  * {@link CreateDefaults.pgdog} or `false`.
65
66
  */
66
67
  readonly pgdog?: boolean;
68
+ /**
69
+ * Pin `images.proxy`. When omitted, uses {@link CreateDefaults.proxy} or
70
+ * `none`.
71
+ */
72
+ readonly proxy?: CreateProxyId;
67
73
  };
68
74
 
69
75
  /** Result of a successful scaffold. */
@@ -82,6 +88,8 @@ export type ScaffoldResult = {
82
88
  readonly locales: readonly string[];
83
89
  /** Whether `images.pgdog` was pinned. */
84
90
  readonly pgdog: boolean;
91
+ /** Proxy wizard id applied (`none` = unset). */
92
+ readonly proxy: CreateProxyId;
85
93
  /** Relative paths written (POSIX), sorted. */
86
94
  readonly files: readonly string[];
87
95
  };
@@ -178,10 +186,13 @@ export async function scaffold(options: ScaffoldOptions): Promise<ScaffoldResult
178
186
  }
179
187
 
180
188
  const pgdog = options.pgdog ?? createDefaults?.pgdog ?? false;
189
+ const proxy = options.proxy ?? createDefaults?.proxy ?? "none";
181
190
  const configPath = join(targetDir, "oke.config.ts");
182
191
  if (existsSync(configPath)) {
192
+ let next = readFileSync(configPath, "utf8");
193
+ next = applyPgDogToConfig(next, pgdog);
194
+ next = applyProxyToConfig(next, proxy);
183
195
  const prev = readFileSync(configPath, "utf8");
184
- const next = applyPgDogToConfig(prev, pgdog);
185
196
  if (next !== prev) writeFileSync(configPath, next, "utf8");
186
197
  }
187
198
 
@@ -204,6 +215,7 @@ export async function scaffold(options: ScaffoldOptions): Promise<ScaffoldResult
204
215
  ...(createDefaults !== undefined ? { createDefaults } : {}),
205
216
  locales,
206
217
  pgdog,
218
+ proxy,
207
219
  files: written,
208
220
  };
209
221
  } catch (e) {
@@ -393,7 +405,7 @@ services:
393
405
  ports:
394
406
  - "6530:6530"
395
407
  env_file:
396
- - .env.docker
408
+ - ../.env.local
397
409
  depends_on:
398
410
  store-sql:
399
411
  condition: service_healthy
@@ -427,7 +439,7 @@ services:
427
439
  networks:
428
440
  - oke
429
441
  env_file:
430
- - .env.docker
442
+ - ../.env.local
431
443
  environment:
432
444
  POSTGRES_USER: \${OKE_STORE_SQL_USER}
433
445
  POSTGRES_PASSWORD: \${OKE_STORE_SQL_PASSWORD}
@@ -453,7 +465,7 @@ services:
453
465
  networks:
454
466
  - oke
455
467
  env_file:
456
- - .env.docker
468
+ - ../.env.local
457
469
  command:
458
470
  - sh
459
471
  - -c