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 +1 -1
- package/src/ai-setup/apply.ts +10 -14
- package/src/transform.test.ts +7 -3
- package/src/transform.ts +93 -22
- package/templates/advanced/oke.config.ts +10 -6
- package/templates/advanced/src/core/store.ts +1 -1
- package/templates/advanced/src/flows/notes/index.ts +2 -2
- package/templates/advanced/tsconfig.json +3 -0
- package/templates/standard/oke.config.ts +9 -5
- package/templates/standard/src/core/store.ts +1 -1
- package/templates/standard/src/flows/notes/index.ts +2 -2
- package/templates/standard/tsconfig.json +3 -0
package/package.json
CHANGED
package/src/ai-setup/apply.ts
CHANGED
|
@@ -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
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
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}. */
|
package/src/transform.test.ts
CHANGED
|
@@ -21,7 +21,11 @@ function evalConfig(source: string): {
|
|
|
21
21
|
ai?: unknown;
|
|
22
22
|
channel?: { ai?: unknown; email?: unknown };
|
|
23
23
|
};
|
|
24
|
-
images?:
|
|
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).
|
|
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).
|
|
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
|
-
*
|
|
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
|
|
385
|
-
if (!
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
|
393
|
-
if (IMAGE_ENV_COLUMNS.has(
|
|
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 -
|
|
477
|
+
* @param images - Dotted role → image
|
|
402
478
|
*/
|
|
403
|
-
function replaceImagesBlock(source: string, images: Record<string, string>): string {
|
|
404
|
-
const
|
|
405
|
-
|
|
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.
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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,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 "
|
|
5
|
-
import { notes } from "
|
|
4
|
+
import { db, files, noteCreatedMail, webhookSecret } from "@/core";
|
|
5
|
+
import { notes } from "@/db/schema.decl";
|
|
6
6
|
import {
|
|
7
7
|
NoteAttachIn,
|
|
8
8
|
NoteAttachOut,
|
|
@@ -64,12 +64,16 @@ export default defineConfig({
|
|
|
64
64
|
},
|
|
65
65
|
},
|
|
66
66
|
images: {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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,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 "
|
|
5
|
-
import { notes } from "
|
|
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
|
|