create-oke 0.10.2 → 0.10.3

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-oke",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
4
4
  "description": "Scaffold an okengine app — bunx create-oke@latest <name>",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -5,6 +5,7 @@
5
5
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
6
  import { dirname, join } from "node:path";
7
7
  import { OLLAMA_IMAGE } from "../drivers-catalog.ts";
8
+ import { extractImages, findImagesBlock, replaceImagesBlock } from "../transform.ts";
8
9
 
9
10
  /** Choices applied to the project. */
10
11
  export type AiSetupApplyInput = {
@@ -127,24 +128,19 @@ export function upsertAiDrivers(source: string, driver: string): string {
127
128
  }
128
129
 
129
130
  /**
131
+ * Set one dotted role's image pin, preserving every other pin (including
132
+ * `store.*` / `channel.*` nesting) via {@link extractImages} /
133
+ * {@link replaceImagesBlock} — a parse/set/render round-trip rather than a
134
+ * single-line regex, so nested sub-objects are never corrupted.
135
+ *
130
136
  * @param source - Config source
131
- * @param key - Image role
137
+ * @param key - Image role (dotted for `store.*` / `channel.*`, flat otherwise)
132
138
  * @param image - Image ref
133
139
  */
134
140
  export function upsertImage(source: string, key: string, image: string): string {
135
- const keyLit = key.includes(".") ? `"${key}"` : key;
136
- const line = ` ${keyLit}: "${image}",`;
137
- const imagesRe = /images:\s*\{([\s\S]*?)\n\s*\}/;
138
- const m = imagesRe.exec(source);
139
- if (!m) return source;
140
- const body = m[1]!;
141
- if (new RegExp(`${keyLit}\\s*:`).test(body) || new RegExp(`"${key}"\\s*:`).test(body)) {
142
- return source.replace(
143
- new RegExp(`(["']?${key.replace(".", "\\.")}["']?\\s*:\\s*)"[^"]*"`),
144
- `$1"${image}"`,
145
- );
146
- }
147
- return source.replace(imagesRe, `images: {${body}\n${line}\n }`);
141
+ if (!findImagesBlock(source)) return source;
142
+ const images = { ...extractImages(source), [key]: image };
143
+ return replaceImagesBlock(source, images);
148
144
  }
149
145
 
150
146
  /** Options for {@link upsertEnv}. */
@@ -21,7 +21,11 @@ function evalConfig(source: string): {
21
21
  ai?: unknown;
22
22
  channel?: { ai?: unknown; email?: unknown };
23
23
  };
24
- images?: Record<string, string>;
24
+ images?: {
25
+ store?: Record<string, string>;
26
+ channel?: Record<string, string>;
27
+ ai?: string;
28
+ };
25
29
  } {
26
30
  const body = source
27
31
  .replace(/^import\s+[\s\S]*?from\s+["'][^"']+["'];?\s*/m, "")
@@ -60,7 +64,7 @@ describe("applyCreateAnswers images", () => {
60
64
  expect(next).not.toMatch(/images:\s*\{[^}]*\blocal:\s*"/s);
61
65
  expect(next).not.toMatch(/images:\s*\{[^}]*\bdocker:\s*"/s);
62
66
  expect(next).not.toMatch(/images:\s*\{[^}]*:\s*"libsql"/s);
63
- expect(next).toContain('"store.sql": "postgres:18-alpine"');
67
+ expect(next).toMatch(/images:\s*\{\s*store:\s*\{[^}]*\bsql: "postgres:18-alpine"/s);
64
68
  });
65
69
 
66
70
  test("index meilisearch pins store.index image without comment leakage", () => {
@@ -68,7 +72,7 @@ describe("applyCreateAnswers images", () => {
68
72
  templateConfig(),
69
73
  defaultsWithIndex("meilisearch", "meilisearch"),
70
74
  );
71
- expect(next).toContain('"store.index": "getmeili/meilisearch:v1.37"');
75
+ expect(next).toMatch(/images:\s*\{\s*store:\s*\{[^}]*\bindex: "getmeili\/meilisearch:v1.37"/s);
72
76
  expect(next).not.toMatch(/images:\s*\{[^}]*\btest:\s*"memory"/s);
73
77
  });
74
78
  });
package/src/transform.ts CHANGED
@@ -298,8 +298,11 @@ ${close}}`;
298
298
  /** Env-column keys that must never appear under `images`. */
299
299
  const IMAGE_ENV_COLUMNS = new Set(["local", "docker", "test", "prod"]);
300
300
 
301
- /** Known compose role keys written into `images`. */
302
- const IMAGE_ROLE_KEY = /^(?:store\.(?:sql|kv|files|index)|channel\.email|vault|ai|pgdog)$/;
301
+ /** Known compose role keys written into `images` (dotted, post-flatten). */
302
+ const IMAGE_ROLE_KEY = /^(?:store\.(?:sql|kv|files|index)|channel\.email|vault|ai|pgdog|proxy)$/;
303
+
304
+ /** `images` sub-object keys that nest role facets (mirrors `drivers` nesting). */
305
+ const IMAGE_NEST_KEYS = ["store", "channel"] as const;
303
306
 
304
307
  /**
305
308
  * Keep `images` in sync with chosen docker drivers.
@@ -374,43 +377,111 @@ function aiImageForDefaults(defaults: CreateDefaults): string {
374
377
  }
375
378
 
376
379
  /**
377
- * Parse role→image pins from an `images` block.
380
+ * Locate the `images: { }` block by brace depth (not the first `}`) so
381
+ * nested `store: { … }` / `channel: { … }` sub-blocks don't close the match
382
+ * early.
383
+ *
384
+ * @param source - Config source
385
+ */
386
+ export function findImagesBlock(source: string): {
387
+ readonly start: number;
388
+ readonly bodyStart: number;
389
+ readonly bodyEnd: number;
390
+ readonly end: number;
391
+ } | null {
392
+ const m = /images:\s*\{/.exec(source);
393
+ if (!m) return null;
394
+ const start = m.index;
395
+ const openIdx = start + m[0].length - 1;
396
+ let depth = 0;
397
+ for (let i = openIdx; i < source.length; i++) {
398
+ const ch = source[i];
399
+ if (ch === "{") depth++;
400
+ else if (ch === "}") {
401
+ depth--;
402
+ if (depth === 0) return { start, bodyStart: openIdx + 1, bodyEnd: i, end: i + 1 };
403
+ }
404
+ }
405
+ return null;
406
+ }
407
+
408
+ /**
409
+ * Parse dotted role→image pins from an `images` block, flattening one level
410
+ * of `store` / `channel` nesting (mirrors {@link flattenImagesConfig} in
411
+ * `okengine/config`).
378
412
  *
379
413
  * Skips `//` comment lines and rejects env-column keys (`local`/`docker`/…).
380
414
  *
381
415
  * @param source - Config source
382
416
  */
383
- function extractImages(source: string): Record<string, string> {
384
- const m = /images:\s*\{([\s\S]*?)\n\s*\},/.exec(source);
385
- if (!m) return {};
417
+ export function extractImages(source: string): Record<string, string> {
418
+ const block = findImagesBlock(source);
419
+ if (!block) return {};
386
420
  const out: Record<string, string> = {};
387
- for (const line of m[1]!.split("\n")) {
388
- const trimmed = line.trim();
389
- if (!trimmed || trimmed.startsWith("//")) continue;
390
- const hit = /["']?([\w.]+)["']?\s*:\s*"([^"]+)"/.exec(trimmed);
421
+ let context: (typeof IMAGE_NEST_KEYS)[number] | null = null;
422
+ for (const raw of source.slice(block.bodyStart, block.bodyEnd).split("\n")) {
423
+ const line = raw.trim();
424
+ if (!line || line.startsWith("//")) continue;
425
+ const nestOpen = /^(store|channel):\s*\{\s*$/.exec(line);
426
+ if (nestOpen) {
427
+ context = nestOpen[1] as (typeof IMAGE_NEST_KEYS)[number];
428
+ continue;
429
+ }
430
+ if (line === "}," || line === "}") {
431
+ context = null;
432
+ continue;
433
+ }
434
+ const hit = /^["']?([\w.]+)["']?\s*:\s*"([^"]+)"/.exec(line);
391
435
  if (!hit) continue;
392
- const key = hit[1]!;
393
- if (IMAGE_ENV_COLUMNS.has(key) || !IMAGE_ROLE_KEY.test(key)) continue;
436
+ const rawKey = hit[1]!;
437
+ if (IMAGE_ENV_COLUMNS.has(rawKey)) continue;
438
+ const key = context ? `${context}.${rawKey}` : rawKey;
439
+ if (!IMAGE_ROLE_KEY.test(key)) continue;
394
440
  out[key] = hit[2]!;
395
441
  }
396
442
  return out;
397
443
  }
398
444
 
445
+ /**
446
+ * Render dotted role→image pins back into a nested `images: { … }` literal —
447
+ * `store.*` / `channel.*` under their sub-object, everything else flat.
448
+ *
449
+ * @param images - Dotted role → image
450
+ */
451
+ function formatImagesBlock(images: Record<string, string>): string {
452
+ const store: Array<[string, string]> = [];
453
+ const channel: Array<[string, string]> = [];
454
+ const flat: Array<[string, string]> = [];
455
+ for (const [key, value] of Object.entries(images)) {
456
+ if (key.startsWith("store.")) store.push([key.slice("store.".length), value]);
457
+ else if (key.startsWith("channel.")) channel.push([key.slice("channel.".length), value]);
458
+ else flat.push([key, value]);
459
+ }
460
+ const lines: string[] = [];
461
+ if (store.length > 0) {
462
+ lines.push(" store: {");
463
+ for (const [k, v] of store) lines.push(` ${k}: "${v}",`);
464
+ lines.push(" },");
465
+ }
466
+ if (channel.length > 0) {
467
+ lines.push(" channel: {");
468
+ for (const [k, v] of channel) lines.push(` ${k}: "${v}",`);
469
+ lines.push(" },");
470
+ }
471
+ for (const [k, v] of flat) lines.push(` ${k}: "${v}",`);
472
+ return `images: {\n${lines.join("\n")}\n }`;
473
+ }
474
+
399
475
  /**
400
476
  * @param source - Config source
401
- * @param images - Role → image
477
+ * @param images - Dotted role → image
402
478
  */
403
- function replaceImagesBlock(source: string, images: Record<string, string>): string {
404
- const lines = Object.entries(images).map(([k, v]) => {
405
- const key = k.includes(".") ? `"${k}"` : k;
406
- return ` ${key}: "${v}",`;
407
- });
408
- const block = `images: {\n${lines.join("\n")}\n }`;
409
- const re = /images:\s*\{[\s\S]*?\n\s*\}/;
410
- if (!re.test(source)) {
479
+ export function replaceImagesBlock(source: string, images: Record<string, string>): string {
480
+ const block = findImagesBlock(source);
481
+ if (!block) {
411
482
  throw new Error("create-oke: oke.config.ts missing images block");
412
483
  }
413
- return source.replace(re, block);
484
+ return `${source.slice(0, block.start)}${formatImagesBlock(images)}${source.slice(block.end)}`;
414
485
  }
415
486
 
416
487
  /**
@@ -71,13 +71,17 @@ export default defineConfig({
71
71
  // Opt in: create-oke --ai / oke ai setup writes drivers.ai + src/core/ai.ts
72
72
  },
73
73
  images: {
74
- "store.sql": "postgres:18-alpine",
75
- pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.51",
76
- "store.kv": "redis:8-alpine",
77
- "store.files": "rustfs/rustfs:1.0.0-beta.11",
78
- "channel.email": "axllent/mailpit:v1.22.3",
74
+ store: {
75
+ sql: "postgres:18-alpine",
76
+ kv: "redis:8-alpine",
77
+ files: "rustfs/rustfs:1.0.0-beta.11",
78
+ // index: "getmeili/meilisearch:v1.37",
79
+ },
80
+ channel: {
81
+ email: "axllent/mailpit:v1.22.3",
82
+ },
79
83
  vault: "openbao/openbao:2.6.1",
80
- // "store.index": "getmeili/meilisearch:v1.37",
84
+ pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.51",
81
85
  // ai: "ghcr.io/ggml-org/llama.cpp:server-b10290", // or ollama/ollama:0.32.6
82
86
  },
83
87
  i18n: { locales: ["en", "ar"], default: "en", dir: { ar: "rtl" } },
@@ -1,5 +1,5 @@
1
1
  import { store } from "okengine";
2
- import * as schema from "../db/schema.decl";
2
+ import * as schema from "@/db/schema.decl";
3
3
 
4
4
  /** App SQL store — tables from {@link schema}. */
5
5
  export const db = store.sql("app", { schema });
@@ -1,8 +1,8 @@
1
1
  import { on, flow, http, every, gate, fail } from "okengine";
2
2
  import { eq, isNull } from "drizzle-orm";
3
3
 
4
- import { db, files, noteCreatedMail, webhookSecret } from "../../core";
5
- import { notes } from "../../db/schema.decl";
4
+ import { db, files, noteCreatedMail, webhookSecret } from "@/core";
5
+ import { notes } from "@/db/schema.decl";
6
6
  import {
7
7
  NoteAttachIn,
8
8
  NoteAttachOut,
@@ -6,6 +6,9 @@
6
6
  "moduleDetection": "force",
7
7
  "types": ["bun"],
8
8
  "moduleResolution": "bundler",
9
+ "paths": {
10
+ "@/*": ["./src/*"]
11
+ },
9
12
  "allowImportingTsExtensions": true,
10
13
  "verbatimModuleSyntax": true,
11
14
  "noEmit": true,
@@ -64,12 +64,16 @@ export default defineConfig({
64
64
  },
65
65
  },
66
66
  images: {
67
- "store.sql": "postgres:18-alpine",
68
- pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.51",
69
- "store.kv": "redis:8-alpine",
70
- "store.files": "rustfs/rustfs:1.0.0-beta.11",
71
- "channel.email": "axllent/mailpit:v1.22.3",
67
+ store: {
68
+ sql: "postgres:18-alpine",
69
+ kv: "redis:8-alpine",
70
+ files: "rustfs/rustfs:1.0.0-beta.11",
71
+ },
72
+ channel: {
73
+ email: "axllent/mailpit:v1.22.3",
74
+ },
72
75
  vault: "openbao/openbao:2.6.1",
76
+ pgdog: "ghcr.io/pgdogdev/pgdog:v0.1.51",
73
77
  },
74
78
  i18n: { locales: ["en", "ar"], default: "en", dir: { ar: "rtl" } },
75
79
  });
@@ -1,5 +1,5 @@
1
1
  import { store } from "okengine";
2
- import * as schema from "../db/schema.decl";
2
+ import * as schema from "@/db/schema.decl";
3
3
 
4
4
  /** App SQL store — tables from {@link schema}. */
5
5
  export const db = store.sql("app", { schema });
@@ -1,8 +1,8 @@
1
1
  import { on, flow, http, gate, fail } from "okengine";
2
2
  import { eq, isNull } from "drizzle-orm";
3
3
 
4
- import { db, noteCreatedMail, webhookSecret } from "../../core";
5
- import { notes } from "../../db/schema.decl";
4
+ import { db, noteCreatedMail, webhookSecret } from "@/core";
5
+ import { notes } from "@/db/schema.decl";
6
6
  import { NoteCreateIn, NoteIdIn, NoteListOut, NoteOut, NotFound } from "./shapes";
7
7
  import { noteCreated } from "./signals";
8
8
 
@@ -6,6 +6,9 @@
6
6
  "moduleDetection": "force",
7
7
  "types": ["bun"],
8
8
  "moduleResolution": "bundler",
9
+ "paths": {
10
+ "@/*": ["./src/*"]
11
+ },
9
12
  "allowImportingTsExtensions": true,
10
13
  "verbatimModuleSyntax": true,
11
14
  "noEmit": true,