create-oke 0.10.3 → 0.11.1
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/README.md +2 -2
- package/package.json +2 -2
- package/src/ai-setup/apply.ts +113 -17
- package/src/ai-setup/catalog.ts +3 -3
- package/src/ai-setup/from-pref.ts +1 -1
- package/src/ai-setup/prompts.ts +1 -1
- package/src/cli.test.ts +183 -142
- package/src/cli.ts +190 -32
- package/src/create-defaults.test.ts +21 -15
- package/src/create-defaults.ts +33 -13
- package/src/customize-flow.test.ts +30 -35
- package/src/customize-flow.ts +68 -229
- package/src/drivers-catalog.ts +42 -48
- package/src/local-okengine.test.ts +50 -41
- package/src/local-okengine.ts +28 -14
- package/src/locales.test.ts +128 -0
- package/src/locales.ts +321 -0
- package/src/scaffold.ts +277 -33
- package/src/templates.ts +2 -8
- package/src/transform.test.ts +96 -36
- package/src/transform.ts +285 -110
- package/templates/advanced/.env.example +23 -3
- package/templates/advanced/README.md +1 -1
- package/templates/advanced/drizzle.config.ts +10 -13
- package/templates/advanced/oke.config.ts +12 -57
- package/templates/advanced/package.json +3 -1
- package/templates/advanced/src/app.ts +3 -10
- package/templates/advanced/src/core.ts +58 -0
- package/templates/advanced/src/db/schema.decl.ts +1 -1
- package/templates/advanced/src/db/seed/index.ts +2 -2
- package/templates/advanced/src/flows/notes/index.ts +1 -13
- package/templates/advanced/src/locales/index.ts +6 -0
- package/templates/advanced/tests/advanced.test.ts +1 -1
- package/templates/standard/.env.example +23 -3
- package/templates/standard/README.md +1 -1
- package/templates/standard/drizzle.config.ts +10 -13
- package/templates/standard/oke.config.ts +10 -56
- package/templates/standard/package.json +3 -1
- package/templates/standard/src/app.ts +3 -10
- package/templates/standard/src/core.ts +55 -0
- package/templates/standard/src/db/schema.decl.ts +1 -1
- package/templates/standard/src/db/seed/index.ts +2 -2
- package/templates/standard/src/flows/notes/index.ts +1 -10
- package/templates/standard/src/locales/index.ts +6 -0
- package/templates/standard/tests/standard.test.ts +1 -1
- package/templates/advanced/src/core/channels.ts +0 -13
- package/templates/advanced/src/core/gates.ts +0 -12
- package/templates/advanced/src/core/index.ts +0 -12
- package/templates/advanced/src/core/store.ts +0 -8
- package/templates/advanced/src/core/vault.ts +0 -7
- package/templates/advanced/src/locales/ar.ts +0 -18
- package/templates/standard/src/core/channels.ts +0 -13
- package/templates/standard/src/core/gates.ts +0 -12
- package/templates/standard/src/core/index.ts +0 -12
- package/templates/standard/src/core/store.ts +0 -5
- package/templates/standard/src/core/vault.ts +0 -7
- package/templates/standard/src/locales/ar.ts +0 -18
package/src/transform.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* workspace root; registry version otherwise)
|
|
10
10
|
* 3. Drop monorepo-only files that import paths outside the source tree
|
|
11
11
|
* (today: `tests/docker.test.ts`)
|
|
12
|
-
* 4. Optional `--sql` / wizard choice →
|
|
12
|
+
* 4. Optional `--sql` / wizard choice → `store.sql` pins
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { readFileSync } from "node:fs";
|
|
@@ -25,14 +25,14 @@ import {
|
|
|
25
25
|
import { materializeLocalOkengineDependency } from "./local-okengine.ts";
|
|
26
26
|
import { packageRoot } from "./templates.ts";
|
|
27
27
|
|
|
28
|
-
/** SQL store drivers selectable at scaffold time. */
|
|
29
|
-
export const SQL_DRIVERS = ["
|
|
28
|
+
/** SQL store drivers selectable at scaffold time (SQLite removed). */
|
|
29
|
+
export const SQL_DRIVERS = ["postgres"] as const;
|
|
30
30
|
|
|
31
31
|
/** A known store.sql driver id for create-oke. */
|
|
32
32
|
export type SqlDriverId = (typeof SQL_DRIVERS)[number];
|
|
33
33
|
|
|
34
34
|
/** Default when `--sql` / wizard choice is omitted (matches template sources). */
|
|
35
|
-
export const DEFAULT_SQL_DRIVER: SqlDriverId = "
|
|
35
|
+
export const DEFAULT_SQL_DRIVER: SqlDriverId = "postgres";
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Whether `value` is a known {@link SqlDriverId}.
|
|
@@ -124,64 +124,72 @@ export function shouldSkipTemplatePath(relativePath: string): boolean {
|
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
/**
|
|
127
|
-
*
|
|
127
|
+
* Real `DRIVER_DEFAULTS` mirrors (okengine/config) — create-oke has no
|
|
128
|
+
* runtime dependency on okengine, so keep a local copy for sparse pins.
|
|
128
129
|
*
|
|
129
|
-
*
|
|
130
|
-
* `postgres` swaps to `pgTable` / `pg-core` (and the reverse is idempotent).
|
|
131
|
-
*
|
|
132
|
-
* @param source - Schema TypeScript source
|
|
133
|
-
* @param driver - Target store.sql driver
|
|
130
|
+
* Keys with no entry (e.g. `index`, `ai`) always emit every env column.
|
|
134
131
|
*/
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
132
|
+
const PIN_DEFAULTS: Readonly<Partial<Record<string, EnvDriverPins>>> = {
|
|
133
|
+
sql: { dev: "postgres", test: "pglite", prod: "postgres" },
|
|
134
|
+
kv: { dev: "redis", test: "memory", prod: "redis" },
|
|
135
|
+
files: { dev: "s3", test: "memory", prod: "s3" },
|
|
136
|
+
signal: { dev: "redis", test: "memory", prod: "redis" },
|
|
137
|
+
clock: { dev: "postgres", test: "frozen", prod: "postgres" },
|
|
138
|
+
vault: { dev: "env", test: "memory", prod: "vault" },
|
|
139
|
+
email: { dev: "smtp", test: "console", prod: "smtp" },
|
|
140
|
+
};
|
|
145
141
|
|
|
146
142
|
/**
|
|
147
|
-
* Pin `store.sql` `
|
|
143
|
+
* Pin `store.sql` `dev` / `prod` in `oke.config.ts` to `driver`.
|
|
148
144
|
*
|
|
149
|
-
* Leaves `test
|
|
145
|
+
* Leaves `test` (and every other facet) untouched. When the template omits
|
|
146
|
+
* `store.sql` (defaults already match), this is a no-op for `postgres`.
|
|
150
147
|
*
|
|
151
148
|
* @param source - Config TypeScript source
|
|
152
149
|
* @param driver - Target store.sql driver
|
|
153
150
|
*/
|
|
154
151
|
export function transformConfigForSqlDriver(source: string, driver: SqlDriverId): string {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
152
|
+
const sqlRe = /^( {6})sql:\s*\{[\s\S]*?\n\1\}/m;
|
|
153
|
+
if (sqlRe.test(source)) {
|
|
154
|
+
return source.replace(sqlRe, (block) =>
|
|
155
|
+
block
|
|
156
|
+
.replace(/dev:\s*"(?:postgres|pglite|memory)"/, `dev: "${driver}"`)
|
|
157
|
+
.replace(/prod:\s*"(?:postgres|pglite|memory)"/, `prod: "${driver}"`),
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
// Sparse template with no sql override — defaults already cover postgres.
|
|
161
|
+
if (driver === "postgres") return source;
|
|
162
|
+
return upsertStoreFacet(source, "sql", {
|
|
163
|
+
dev: driver,
|
|
164
|
+
test: "pglite",
|
|
165
|
+
prod: driver,
|
|
166
|
+
});
|
|
161
167
|
}
|
|
162
168
|
|
|
163
169
|
/**
|
|
164
170
|
* Apply full customize answers to `oke.config.ts` source.
|
|
165
171
|
*
|
|
166
172
|
* Rewrites driver maps and syncs `images` keys for docker-backed roles.
|
|
173
|
+
* Pins that match {@link PIN_DEFAULTS} are omitted (or removed) so scaffolds
|
|
174
|
+
* stay override-only.
|
|
167
175
|
*
|
|
168
176
|
* @param source - Config TypeScript source
|
|
169
177
|
* @param defaults - Customize / reuse payload
|
|
170
178
|
*/
|
|
171
179
|
export function applyCreateAnswers(source: string, defaults: CreateDefaults): string {
|
|
172
180
|
let next = source;
|
|
173
|
-
next =
|
|
174
|
-
next =
|
|
175
|
-
next =
|
|
181
|
+
next = upsertEnvMap(next, "sql", defaults.drivers.store.sql, { nestedUnderStore: true });
|
|
182
|
+
next = upsertEnvMap(next, "kv", defaults.drivers.store.kv, { nestedUnderStore: true });
|
|
183
|
+
next = upsertEnvMap(next, "files", defaults.drivers.store.files, { nestedUnderStore: true });
|
|
176
184
|
|
|
177
185
|
if (defaults.drivers.store.index) {
|
|
178
186
|
next = upsertStoreIndex(next, defaults.drivers.store.index);
|
|
179
187
|
}
|
|
180
188
|
|
|
181
|
-
next =
|
|
182
|
-
next =
|
|
183
|
-
next =
|
|
184
|
-
next =
|
|
189
|
+
next = upsertEnvMap(next, "signal", defaults.drivers.signal, { nestedUnderStore: false });
|
|
190
|
+
next = upsertEnvMap(next, "clock", defaults.drivers.clock, { nestedUnderStore: false });
|
|
191
|
+
next = upsertEnvMap(next, "vault", defaults.drivers.vault, { nestedUnderStore: false });
|
|
192
|
+
next = upsertChannelEmail(next, defaults.drivers.channel.email);
|
|
185
193
|
|
|
186
194
|
if (defaults.drivers.ai) {
|
|
187
195
|
next = upsertAiDrivers(next, defaults.drivers.ai);
|
|
@@ -192,41 +200,175 @@ export function applyCreateAnswers(source: string, defaults: CreateDefaults): st
|
|
|
192
200
|
}
|
|
193
201
|
|
|
194
202
|
/**
|
|
195
|
-
*
|
|
203
|
+
* Env columns that differ from a known default map (or all columns when none).
|
|
204
|
+
*
|
|
205
|
+
* @param pins - Full three-env pins from the wizard
|
|
206
|
+
* @param defaults - Optional real defaults for this key
|
|
207
|
+
*/
|
|
208
|
+
function sparsePinEntries(
|
|
209
|
+
pins: EnvDriverPins,
|
|
210
|
+
defaults: EnvDriverPins | undefined,
|
|
211
|
+
): ReadonlyArray<readonly ["dev" | "test" | "prod", string]> {
|
|
212
|
+
const envs = ["dev", "test", "prod"] as const;
|
|
213
|
+
if (!defaults) {
|
|
214
|
+
return envs.map((env) => [env, pins[env]] as const);
|
|
215
|
+
}
|
|
216
|
+
return envs.filter((env) => pins[env] !== defaults[env]).map((env) => [env, pins[env]] as const);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Format an env driver map literal — only keys that differ from defaults.
|
|
221
|
+
*
|
|
222
|
+
* @param pins - Pins
|
|
223
|
+
* @param indentLevel - Indent of the map key (`signal` → 2, `sql`/`email` → 3;
|
|
224
|
+
* two spaces each). Inner fields use `indentLevel + 1`; the closing `}`
|
|
225
|
+
* aligns with the key.
|
|
226
|
+
* @param defaultsKey - {@link PIN_DEFAULTS} lookup key (omit for no defaults)
|
|
227
|
+
* @returns Formatted `{ … }` or `null` when every pin matches the default
|
|
228
|
+
*/
|
|
229
|
+
function formatEnvMap(
|
|
230
|
+
pins: EnvDriverPins,
|
|
231
|
+
indentLevel: number,
|
|
232
|
+
defaultsKey?: string,
|
|
233
|
+
): string | null {
|
|
234
|
+
const defaults = defaultsKey !== undefined ? PIN_DEFAULTS[defaultsKey] : undefined;
|
|
235
|
+
const entries = sparsePinEntries(pins, defaults);
|
|
236
|
+
if (entries.length === 0) return null;
|
|
237
|
+
const pad = " ".repeat(indentLevel + 1);
|
|
238
|
+
const close = " ".repeat(indentLevel);
|
|
239
|
+
const lines = entries.map(([env, value]) => `${pad}${env}: "${value}",`);
|
|
240
|
+
return `{\n${lines.join("\n")}\n${close}}`;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Match `key: { … }` at a fixed indentation (store facet = 6, drivers = 4).
|
|
245
|
+
*
|
|
246
|
+
* @param key - Map key
|
|
247
|
+
* @param indent - Leading spaces before the key
|
|
248
|
+
*/
|
|
249
|
+
function envMapRe(key: string, indent: number): RegExp {
|
|
250
|
+
const pad = " ".repeat(indent);
|
|
251
|
+
return new RegExp(`^${pad}${key}:\\s*\\{[\\s\\S]*?\\n${pad}\\}`, "m");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Remove a matched env-map block and its trailing comma / blank line.
|
|
256
|
+
*
|
|
257
|
+
* @param source - Config source
|
|
258
|
+
* @param re - Block matcher
|
|
259
|
+
*/
|
|
260
|
+
function removeMatchedBlock(source: string, re: RegExp): string {
|
|
261
|
+
return source.replace(re, "").replace(/\n{3,}/g, "\n\n");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Rewrite the `drivers: { … }` body, or throw when missing.
|
|
266
|
+
*
|
|
267
|
+
* @param source - Config source
|
|
268
|
+
* @param rewrite - Maps current body → next body
|
|
269
|
+
*/
|
|
270
|
+
function mapDriversBody(source: string, rewrite: (body: string) => string): string {
|
|
271
|
+
const drivers = findBraceBlock(source, /drivers:\s*\{/);
|
|
272
|
+
if (!drivers) {
|
|
273
|
+
throw new Error("create-oke: oke.config.ts missing drivers block");
|
|
274
|
+
}
|
|
275
|
+
const body = source.slice(drivers.bodyStart, drivers.bodyEnd);
|
|
276
|
+
return `${source.slice(0, drivers.bodyStart)}${rewrite(body)}${source.slice(drivers.bodyEnd)}`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Insert `store: { facet }` (or a facet into an existing store) into drivers.
|
|
281
|
+
*
|
|
282
|
+
* Scoped to the `drivers` block so `images.store` is never touched.
|
|
283
|
+
*
|
|
284
|
+
* @param source - Config source
|
|
285
|
+
* @param key - Store facet key
|
|
286
|
+
* @param pins - Pins (full; formatted sparsely inside)
|
|
287
|
+
*/
|
|
288
|
+
function upsertStoreFacet(source: string, key: string, pins: EnvDriverPins): string {
|
|
289
|
+
const block = formatEnvMap(pins, 3, key);
|
|
290
|
+
const facetRe = envMapRe(key, 6);
|
|
291
|
+
if (block === null) {
|
|
292
|
+
return mapDriversBody(source, (body) => removeMatchedBlock(body, facetRe));
|
|
293
|
+
}
|
|
294
|
+
return mapDriversBody(source, (body) => {
|
|
295
|
+
if (facetRe.test(body)) {
|
|
296
|
+
return body.replace(facetRe, ` ${key}: ${block}`);
|
|
297
|
+
}
|
|
298
|
+
const storeRe = /^( {4})store:\s*\{/m;
|
|
299
|
+
if (storeRe.test(body)) {
|
|
300
|
+
return body.replace(storeRe, (open) => `${open}\n ${key}: ${block},`);
|
|
301
|
+
}
|
|
302
|
+
const entry = ` store: {\n ${key}: ${block},\n },\n`;
|
|
303
|
+
return `${body}${body.endsWith("\n") ? "" : "\n"}${entry}`;
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Upsert a top-level or store-nested env map; omit when pins match defaults.
|
|
196
309
|
*
|
|
197
310
|
* @param source - Config source
|
|
198
311
|
* @param key - Map key (`sql`, `signal`, …)
|
|
199
312
|
* @param pins - New pins
|
|
200
313
|
* @param options - Nesting
|
|
201
314
|
*/
|
|
202
|
-
function
|
|
315
|
+
function upsertEnvMap(
|
|
203
316
|
source: string,
|
|
204
317
|
key: string,
|
|
205
318
|
pins: EnvDriverPins,
|
|
206
319
|
options: { readonly nestedUnderStore: boolean },
|
|
207
320
|
): string {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const re = new RegExp(`${key}:\\s*\\{[\\s\\S]*?\\n\\s*\\}`, "m");
|
|
211
|
-
if (!re.test(source)) {
|
|
212
|
-
throw new Error(`create-oke: oke.config.ts missing drivers.${key} block`);
|
|
321
|
+
if (options.nestedUnderStore) {
|
|
322
|
+
return upsertStoreFacet(source, key, pins);
|
|
213
323
|
}
|
|
214
|
-
|
|
324
|
+
const indent = 2;
|
|
325
|
+
const block = formatEnvMap(pins, indent, key);
|
|
326
|
+
const re = envMapRe(key, 4);
|
|
327
|
+
if (block === null) {
|
|
328
|
+
return mapDriversBody(source, (body) => removeMatchedBlock(body, re));
|
|
329
|
+
}
|
|
330
|
+
return mapDriversBody(source, (body) => {
|
|
331
|
+
if (re.test(body)) {
|
|
332
|
+
return body.replace(re, ` ${key}: ${block}`);
|
|
333
|
+
}
|
|
334
|
+
const entry = ` ${key}: ${block},\n`;
|
|
335
|
+
return `${body}${body.endsWith("\n") ? "" : "\n"}${entry}`;
|
|
336
|
+
});
|
|
215
337
|
}
|
|
216
338
|
|
|
217
339
|
/**
|
|
218
|
-
*
|
|
340
|
+
* Upsert `drivers.channel.email` (creates `drivers.channel` when needed).
|
|
341
|
+
*
|
|
342
|
+
* Never touches `images.channel` (string role pins at the same indent).
|
|
219
343
|
*
|
|
220
344
|
* @param source - Config source
|
|
221
345
|
* @param pins - Email pins
|
|
222
346
|
*/
|
|
223
|
-
function
|
|
224
|
-
const block = formatEnvMap(pins, 3);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
347
|
+
function upsertChannelEmail(source: string, pins: EnvDriverPins): string {
|
|
348
|
+
const block = formatEnvMap(pins, 3, "email");
|
|
349
|
+
// Brace-map `email: { … }` only exists under drivers.channel — images uses
|
|
350
|
+
// `email: "image:tag"`.
|
|
351
|
+
const emailRe = envMapRe("email", 6);
|
|
352
|
+
if (block === null) {
|
|
353
|
+
let next = removeMatchedBlock(source, emailRe);
|
|
354
|
+
next = next.replace(/^( {4})channel:\s*\{\s*\n\1\},?\n/m, "");
|
|
355
|
+
return next;
|
|
228
356
|
}
|
|
229
|
-
|
|
357
|
+
if (emailRe.test(source)) {
|
|
358
|
+
return source.replace(emailRe, ` email: ${block}`);
|
|
359
|
+
}
|
|
360
|
+
const drivers = findBraceBlock(source, /drivers:\s*\{/);
|
|
361
|
+
if (drivers) {
|
|
362
|
+
const body = source.slice(drivers.bodyStart, drivers.bodyEnd);
|
|
363
|
+
if (/^ {4}channel:\s*\{/m.test(body)) {
|
|
364
|
+
const nextBody = body.replace(
|
|
365
|
+
/^( {4})channel:\s*\{/m,
|
|
366
|
+
(open) => `${open}\n email: ${block},`,
|
|
367
|
+
);
|
|
368
|
+
return `${source.slice(0, drivers.bodyStart)}${nextBody}${source.slice(drivers.bodyEnd)}`;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return insertDriversEntry(source, ` channel: {\n email: ${block},\n },`);
|
|
230
372
|
}
|
|
231
373
|
|
|
232
374
|
/**
|
|
@@ -239,64 +381,80 @@ function replaceChannelEmail(source: string, pins: EnvDriverPins): string {
|
|
|
239
381
|
* @param pins - Index pins
|
|
240
382
|
*/
|
|
241
383
|
function upsertStoreIndex(source: string, pins: EnvDriverPins): string {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (indexRe.test(source)) {
|
|
245
|
-
return source.replace(indexRe, `$1index: ${block}`);
|
|
246
|
-
}
|
|
247
|
-
// Insert after files block inside store.
|
|
248
|
-
const filesRe = /(files:\s*\{[\s\S]*?\n\s*\},)/;
|
|
249
|
-
if (!filesRe.test(source)) {
|
|
250
|
-
throw new Error("create-oke: oke.config.ts missing store.files to insert index");
|
|
251
|
-
}
|
|
252
|
-
return source.replace(filesRe, `$1\n index: ${block},`);
|
|
384
|
+
// No DRIVER_DEFAULTS table — emit every env column.
|
|
385
|
+
return upsertStoreFacet(source, "index", pins);
|
|
253
386
|
}
|
|
254
387
|
|
|
255
388
|
/**
|
|
256
|
-
* Insert
|
|
257
|
-
*
|
|
258
|
-
* Indentation-anchored: only matches `ai:` / `channel:` at the drivers-body
|
|
259
|
-
* indent (4 spaces). The old non-greedy `channel: { … },` regex closed on
|
|
260
|
-
* `email`'s `},` and nested `ai` inside `channel` — hero then showed `ai: —`.
|
|
389
|
+
* Insert a drivers-body entry before the closing `}` of `drivers: { … }`.
|
|
261
390
|
*
|
|
262
391
|
* @param source - Config source
|
|
263
|
-
* @param
|
|
392
|
+
* @param entry - Full indented entry including trailing comma
|
|
264
393
|
*/
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
394
|
+
function insertDriversEntry(source: string, entry: string): string {
|
|
395
|
+
return mapDriversBody(source, (body) => {
|
|
396
|
+
const needsNl = !body.endsWith("\n");
|
|
397
|
+
return `${body}${needsNl ? "\n" : ""}${entry}\n`;
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Locate a `{ … }` block by opener regex and brace depth.
|
|
403
|
+
*
|
|
404
|
+
* @param source - Source text
|
|
405
|
+
* @param openRe - Matches through the opening `{`
|
|
406
|
+
*/
|
|
407
|
+
function findBraceBlock(
|
|
408
|
+
source: string,
|
|
409
|
+
openRe: RegExp,
|
|
410
|
+
): {
|
|
411
|
+
readonly start: number;
|
|
412
|
+
readonly bodyStart: number;
|
|
413
|
+
readonly bodyEnd: number;
|
|
414
|
+
readonly end: number;
|
|
415
|
+
} | null {
|
|
416
|
+
const m = openRe.exec(source);
|
|
417
|
+
if (!m) return null;
|
|
418
|
+
const start = m.index;
|
|
419
|
+
const openIdx = start + m[0].length - 1;
|
|
420
|
+
let depth = 0;
|
|
421
|
+
for (let i = openIdx; i < source.length; i++) {
|
|
422
|
+
const ch = source[i];
|
|
423
|
+
if (ch === "{") depth++;
|
|
424
|
+
else if (ch === "}") {
|
|
425
|
+
depth--;
|
|
426
|
+
if (depth === 0) return { start, bodyStart: openIdx + 1, bodyEnd: i, end: i + 1 };
|
|
427
|
+
}
|
|
274
428
|
}
|
|
275
|
-
return
|
|
429
|
+
return null;
|
|
276
430
|
}
|
|
277
431
|
|
|
278
432
|
/**
|
|
279
|
-
*
|
|
433
|
+
* Insert or replace top-level `drivers.ai` (never under `channel` / `images`).
|
|
280
434
|
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
435
|
+
* Replaces an existing `drivers.ai` map; otherwise appends inside `drivers`
|
|
436
|
+
* so sparse templates (no `drivers.channel`) cannot match `images.channel`.
|
|
437
|
+
*
|
|
438
|
+
* @param source - Config source
|
|
439
|
+
* @param pins - AI pins
|
|
286
440
|
*/
|
|
287
|
-
function
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
441
|
+
export function upsertAiDrivers(source: string, pins: EnvDriverPins): string {
|
|
442
|
+
// AI has no DRIVER_DEFAULTS — always emit all three envs.
|
|
443
|
+
const block = formatEnvMap(pins, 2);
|
|
444
|
+
if (block === null) {
|
|
445
|
+
throw new Error("create-oke: ai pins must not be empty");
|
|
446
|
+
}
|
|
447
|
+
const aiRe = envMapRe("ai", 4);
|
|
448
|
+
return mapDriversBody(source, (body) => {
|
|
449
|
+
if (aiRe.test(body)) {
|
|
450
|
+
return body.replace(aiRe, ` ai: ${block}`);
|
|
451
|
+
}
|
|
452
|
+
return `${body}${body.endsWith("\n") ? "" : "\n"} ai: ${block},\n`;
|
|
453
|
+
});
|
|
296
454
|
}
|
|
297
455
|
|
|
298
456
|
/** Env-column keys that must never appear under `images`. */
|
|
299
|
-
const IMAGE_ENV_COLUMNS = new Set(["
|
|
457
|
+
const IMAGE_ENV_COLUMNS = new Set(["dev", "test", "prod"]);
|
|
300
458
|
|
|
301
459
|
/** Known compose role keys written into `images` (dotted, post-flatten). */
|
|
302
460
|
const IMAGE_ROLE_KEY = /^(?:store\.(?:sql|kv|files|index)|channel\.email|vault|ai|pgdog|proxy)$/;
|
|
@@ -322,35 +480,34 @@ function syncImages(source: string, defaults: CreateDefaults): string {
|
|
|
322
480
|
};
|
|
323
481
|
|
|
324
482
|
if (
|
|
325
|
-
d.store.sql.
|
|
483
|
+
d.store.sql.dev === "postgres" ||
|
|
326
484
|
d.store.sql.prod === "postgres" ||
|
|
327
|
-
d.store.sql.
|
|
485
|
+
d.store.sql.dev === "pgvector" ||
|
|
328
486
|
d.store.sql.prod === "pgvector"
|
|
329
487
|
) {
|
|
330
488
|
pin("store.sql", DEFAULT_IMAGES["store.sql"]!);
|
|
331
|
-
|
|
489
|
+
if (defaults.pgdog) {
|
|
490
|
+
pin("pgdog", DEFAULT_IMAGES.pgdog!);
|
|
491
|
+
}
|
|
332
492
|
}
|
|
333
|
-
if (d.store.kv.
|
|
493
|
+
if (d.store.kv.dev === "redis" || d.signal.dev === "redis") {
|
|
334
494
|
pin("store.kv", DEFAULT_IMAGES["store.kv"]!);
|
|
335
495
|
}
|
|
336
|
-
if (d.store.files.
|
|
496
|
+
if (d.store.files.dev === "s3") {
|
|
337
497
|
pin("store.files", DEFAULT_IMAGES["store.files"]!);
|
|
338
498
|
}
|
|
339
|
-
if (d.channel.email.
|
|
499
|
+
if (d.channel.email.dev === "smtp") {
|
|
340
500
|
pin("channel.email", DEFAULT_IMAGES["channel.email"]!);
|
|
341
501
|
}
|
|
342
|
-
if (d.
|
|
343
|
-
pin("vault", DEFAULT_IMAGES.vault!);
|
|
344
|
-
}
|
|
345
|
-
if (d.store.index?.docker === "meilisearch") {
|
|
502
|
+
if (d.store.index?.dev === "meilisearch") {
|
|
346
503
|
pin("store.index", DEFAULT_IMAGES["store.index"]!);
|
|
347
504
|
}
|
|
348
505
|
if (
|
|
349
506
|
d.ai &&
|
|
350
|
-
(d.ai.
|
|
351
|
-
d.ai.
|
|
352
|
-
d.ai.
|
|
353
|
-
d.ai.
|
|
507
|
+
(d.ai.dev === "ollama" ||
|
|
508
|
+
d.ai.prod === "ollama" ||
|
|
509
|
+
d.ai.dev === "openai-compatible" ||
|
|
510
|
+
d.ai.prod === "openai-compatible")
|
|
354
511
|
) {
|
|
355
512
|
pin("ai", aiImageForDefaults(defaults));
|
|
356
513
|
}
|
|
@@ -365,12 +522,12 @@ function syncImages(source: string, defaults: CreateDefaults): string {
|
|
|
365
522
|
*/
|
|
366
523
|
function aiImageForDefaults(defaults: CreateDefaults): string {
|
|
367
524
|
const provider = defaults.ai.provider;
|
|
368
|
-
if (provider === "ollama" || defaults.drivers.ai?.
|
|
525
|
+
if (provider === "ollama" || defaults.drivers.ai?.dev === "ollama") {
|
|
369
526
|
return OLLAMA_IMAGE;
|
|
370
527
|
}
|
|
371
528
|
if (provider === "vllm") return VLLM_IMAGE;
|
|
372
529
|
if (provider === "sglang") return SGLANG_IMAGE;
|
|
373
|
-
if (provider === "llama-cpp" || defaults.drivers.ai?.
|
|
530
|
+
if (provider === "llama-cpp" || defaults.drivers.ai?.dev === "openai-compatible") {
|
|
374
531
|
return LLAMA_CPP_IMAGE;
|
|
375
532
|
}
|
|
376
533
|
return DEFAULT_IMAGES.ai!;
|
|
@@ -410,7 +567,7 @@ export function findImagesBlock(source: string): {
|
|
|
410
567
|
* of `store` / `channel` nesting (mirrors {@link flattenImagesConfig} in
|
|
411
568
|
* `okengine/config`).
|
|
412
569
|
*
|
|
413
|
-
* Skips `//` comment lines and rejects env-column keys (`
|
|
570
|
+
* Skips `//` comment lines and rejects env-column keys (`dev`/`test`/`prod`).
|
|
414
571
|
*
|
|
415
572
|
* @param source - Config source
|
|
416
573
|
*/
|
|
@@ -484,6 +641,24 @@ export function replaceImagesBlock(source: string, images: Record<string, string
|
|
|
484
641
|
return `${source.slice(0, block.start)}${formatImagesBlock(images)}${source.slice(block.end)}`;
|
|
485
642
|
}
|
|
486
643
|
|
|
644
|
+
/**
|
|
645
|
+
* Add or remove the `images.pgdog` pin (PgDog in front of Postgres).
|
|
646
|
+
*
|
|
647
|
+
* @param source - `oke.config.ts` source
|
|
648
|
+
* @param enabled - When true, pin the default PgDog image
|
|
649
|
+
*/
|
|
650
|
+
export function applyPgDogToConfig(source: string, enabled: boolean): string {
|
|
651
|
+
const images = extractImages(source);
|
|
652
|
+
if (enabled) {
|
|
653
|
+
if (images.pgdog) return source;
|
|
654
|
+
images.pgdog = DEFAULT_IMAGES.pgdog!;
|
|
655
|
+
} else {
|
|
656
|
+
if (!images.pgdog) return source;
|
|
657
|
+
delete images.pgdog;
|
|
658
|
+
}
|
|
659
|
+
return replaceImagesBlock(source, images);
|
|
660
|
+
}
|
|
661
|
+
|
|
487
662
|
/**
|
|
488
663
|
* Sanitize a directory / package name for npm.
|
|
489
664
|
*
|
|
@@ -6,6 +6,25 @@
|
|
|
6
6
|
# --- Vault (app secrets) ---
|
|
7
7
|
# Declare secrets in src/vault.ts with `vault.secret(...)`, then uncomment lines here.
|
|
8
8
|
#
|
|
9
|
+
# ── vault — built-in encrypted-at-rest store ────────────────
|
|
10
|
+
# `drivers.vault` is "vault": secrets live encrypted in Postgres, no extra service.
|
|
11
|
+
# Nothing below is required to start — the dotenv layers above resolve first.
|
|
12
|
+
#
|
|
13
|
+
# oke vault init # creates the store, prints the master key ONCE
|
|
14
|
+
# export OKE_VAULT_MASTER_KEY=… # every later command / boot unseals with this
|
|
15
|
+
# oke vault status # sealed / initialized / kek generation
|
|
16
|
+
# oke vault audit # hash-chained operation log
|
|
17
|
+
#
|
|
18
|
+
# Keep the master key out of this file in production — read it from your KMS or
|
|
19
|
+
# secret manager at boot. Committing it defeats encryption at rest.
|
|
20
|
+
# OKE_VAULT_MASTER_KEY=
|
|
21
|
+
#
|
|
22
|
+
# For `drivers.vault: "managed"` instead, point at your provider secret store:
|
|
23
|
+
# OKE_VAULT_PROVIDER=aws-secrets-manager
|
|
24
|
+
# OKE_VAULT_REGION=us-east-1
|
|
25
|
+
# Optional prefix for secret names (defaults to oke/<env>/):
|
|
26
|
+
# OKE_VAULT_MOUNT=oke/prod/
|
|
27
|
+
#
|
|
9
28
|
# --- Infrastructure ---
|
|
10
29
|
# Same keys as `docker/.env.docker` after `oke dev --docker`.
|
|
11
30
|
# Uncomment to override compose credentials for a host-managed service.
|
|
@@ -87,8 +106,9 @@
|
|
|
87
106
|
# POSTGRES_INITDB_ARGS=--data-checksums
|
|
88
107
|
|
|
89
108
|
# ── AI ──────────────────────────────────────────────────────
|
|
90
|
-
#
|
|
91
|
-
#
|
|
92
|
-
#
|
|
109
|
+
# Default local: llama.cpp → openai-compatible (Ollama remains supported)
|
|
110
|
+
# OKE_AI_DRIVER=openai-compatible
|
|
111
|
+
# OKE_AI_URL=http://127.0.0.1:8080/v1
|
|
112
|
+
# OKE_AI_MODEL=granite3.3:2b
|
|
93
113
|
# ANTHROPIC_API_KEY=
|
|
94
114
|
# OPENAI_API_KEY=
|
|
@@ -46,5 +46,5 @@ Without AI, summarize returns a local excerpt (`via: "fallback"`).
|
|
|
46
46
|
|
|
47
47
|
## Drivers
|
|
48
48
|
|
|
49
|
-
See `oke.config.ts`: local
|
|
49
|
+
See `oke.config.ts`: local env/memory/fs · docker postgres/redis/s3/smtp; vault built-in.
|
|
50
50
|
Optional Meilisearch index pin is commented — enable via create-oke customize.
|
|
@@ -3,25 +3,22 @@ import { defineConfig } from "drizzle-kit";
|
|
|
3
3
|
/**
|
|
4
4
|
* Domain schema sync for `oke db push|generate|migrate`.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* for the active dialect as a pre-step of `oke db` / `oke dev`.
|
|
6
|
+
* SQL is PostgreSQL-only (postgres / pglite). Domain tables come from
|
|
7
|
+
* `src/db/schema.decl.ts` → `src/db/schema.drizzle.ts` (emitted as a
|
|
8
|
+
* pre-step of `oke db` / `oke dev`).
|
|
10
9
|
*
|
|
11
|
-
*
|
|
10
|
+
* System / auth / plugin stubs are separate: `oke schema generate` →
|
|
11
|
+
* `.oke/schema/oke.ts` (gitignored with `.oke/`). Re-run after adding plugins.
|
|
12
|
+
*
|
|
13
|
+
* Versioned prod migrations land in `./src/db/migrations`.
|
|
12
14
|
*/
|
|
13
|
-
const dialect = (process.env.OKE_DRIZZLE_DIALECT ?? "sqlite") as "sqlite" | "postgresql";
|
|
14
|
-
|
|
15
15
|
export default defineConfig({
|
|
16
|
-
dialect,
|
|
17
|
-
schema: "./src/db/schema.
|
|
16
|
+
dialect: "postgresql",
|
|
17
|
+
schema: "./src/db/schema.drizzle.ts",
|
|
18
18
|
out: "./src/db/migrations",
|
|
19
19
|
// Core runtime tables (oke_crons, …) are created by drivers — not domain schema.
|
|
20
20
|
tablesFilter: ["!oke_*"],
|
|
21
21
|
dbCredentials: {
|
|
22
|
-
url:
|
|
23
|
-
dialect === "postgresql"
|
|
24
|
-
? (process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL!)
|
|
25
|
-
: (process.env.OKE_SQLITE_URL ?? "file:.oke/app.sqlite"),
|
|
22
|
+
url: process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL!,
|
|
26
23
|
},
|
|
27
24
|
});
|