create-oke 0.10.3 → 0.11.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.
- 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/scaffold.ts
CHANGED
|
@@ -13,17 +13,17 @@ import {
|
|
|
13
13
|
writeFileSync,
|
|
14
14
|
} from "node:fs";
|
|
15
15
|
import { basename, join, relative, resolve } from "node:path";
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
resolveTemplateDir,
|
|
19
|
-
TEMPLATE_DEFAULT_MODE,
|
|
20
|
-
type TemplateId,
|
|
21
|
-
} from "./templates.ts";
|
|
16
|
+
import { pathToFileURL } from "node:url";
|
|
17
|
+
import { resolveLocalOkengineRoot, resolveTemplateDir, type TemplateId } from "./templates.ts";
|
|
22
18
|
import { agentsMdContent } from "./agents-md.ts";
|
|
23
19
|
import type { CreateDefaults } from "./create-defaults.ts";
|
|
20
|
+
import { DEFAULT_IMAGES, TEMPLATE_DEV } from "./drivers-catalog.ts";
|
|
21
|
+
import { applyLocalesToProject } from "./locales.ts";
|
|
24
22
|
import {
|
|
25
23
|
DEFAULT_SQL_DRIVER,
|
|
26
24
|
applyCreateAnswers,
|
|
25
|
+
applyPgDogToConfig,
|
|
26
|
+
extractImages,
|
|
27
27
|
sanitizeProjectName,
|
|
28
28
|
shouldSkipTemplatePath,
|
|
29
29
|
transformConfigForSqlDriver,
|
|
@@ -47,13 +47,23 @@ export type ScaffoldOptions = {
|
|
|
47
47
|
/** Write root `AGENTS.md` (default true). */
|
|
48
48
|
readonly writeAgentsMd?: boolean;
|
|
49
49
|
/**
|
|
50
|
-
* Store SQL driver — pins `oke.config.ts` `store.sql`
|
|
51
|
-
* when
|
|
50
|
+
* Store SQL driver — pins `oke.config.ts` `store.sql` `dev`/`prod`
|
|
51
|
+
* when set. Default `postgres` matches the Docker-first templates.
|
|
52
52
|
* Ignored when {@link createDefaults} is set.
|
|
53
53
|
*/
|
|
54
54
|
readonly sqlDriver?: SqlDriverId;
|
|
55
55
|
/** Full customize / reuse answers — applied after copy. */
|
|
56
56
|
readonly createDefaults?: CreateDefaults;
|
|
57
|
+
/**
|
|
58
|
+
* Extra locales beyond English. When omitted, uses
|
|
59
|
+
* {@link CreateDefaults.locales} or English-only.
|
|
60
|
+
*/
|
|
61
|
+
readonly locales?: readonly string[];
|
|
62
|
+
/**
|
|
63
|
+
* Pin PgDog in front of Postgres. When omitted, uses
|
|
64
|
+
* {@link CreateDefaults.pgdog} or `false`.
|
|
65
|
+
*/
|
|
66
|
+
readonly pgdog?: boolean;
|
|
57
67
|
};
|
|
58
68
|
|
|
59
69
|
/** Result of a successful scaffold. */
|
|
@@ -68,6 +78,10 @@ export type ScaffoldResult = {
|
|
|
68
78
|
readonly sqlDriver: SqlDriverId;
|
|
69
79
|
/** Customize answers applied, if any. */
|
|
70
80
|
readonly createDefaults?: CreateDefaults;
|
|
81
|
+
/** Extra locales applied beyond English. */
|
|
82
|
+
readonly locales: readonly string[];
|
|
83
|
+
/** Whether `images.pgdog` was pinned. */
|
|
84
|
+
readonly pgdog: boolean;
|
|
71
85
|
/** Relative paths written (POSIX), sorted. */
|
|
72
86
|
readonly files: readonly string[];
|
|
73
87
|
};
|
|
@@ -101,14 +115,14 @@ export function targetDirectoryBlockReason(targetDir: string): string | null {
|
|
|
101
115
|
*
|
|
102
116
|
* @param options - Name, source, destination
|
|
103
117
|
*/
|
|
104
|
-
export function scaffold(options: ScaffoldOptions): ScaffoldResult {
|
|
118
|
+
export async function scaffold(options: ScaffoldOptions): Promise<ScaffoldResult> {
|
|
105
119
|
const name = sanitizeProjectName(options.name);
|
|
106
120
|
const targetDir = resolve(options.targetDir);
|
|
107
121
|
const sourceDir = resolveTemplateDir(options.source.id);
|
|
108
122
|
const label = options.source.id;
|
|
109
123
|
const createDefaults = options.createDefaults;
|
|
110
124
|
const sqlDriver =
|
|
111
|
-
createDefaults?.drivers.store.sql.
|
|
125
|
+
createDefaults?.drivers.store.sql.dev === "postgres" || options.sqlDriver === "postgres"
|
|
112
126
|
? "postgres"
|
|
113
127
|
: (options.sqlDriver ?? DEFAULT_SQL_DRIVER);
|
|
114
128
|
|
|
@@ -144,6 +158,8 @@ export function scaffold(options: ScaffoldOptions): ScaffoldResult {
|
|
|
144
158
|
if (!written.includes("AGENTS.md")) written.push("AGENTS.md");
|
|
145
159
|
}
|
|
146
160
|
|
|
161
|
+
ensureVaultEnvNotes(targetDir, createDefaults?.drivers.vault.dev ?? TEMPLATE_DEV.vault);
|
|
162
|
+
|
|
147
163
|
const envExample = join(targetDir, ".env.example");
|
|
148
164
|
const envLocal = join(targetDir, ".env.local");
|
|
149
165
|
if (existsSync(envExample) && !existsSync(envLocal)) {
|
|
@@ -151,9 +167,31 @@ export function scaffold(options: ScaffoldOptions): ScaffoldResult {
|
|
|
151
167
|
if (!written.includes(".env.local")) written.push(".env.local");
|
|
152
168
|
}
|
|
153
169
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
170
|
+
const locales = options.locales ?? createDefaults?.locales ?? [];
|
|
171
|
+
for (const rel of applyLocalesToProject(targetDir, locales)) {
|
|
172
|
+
if (!written.includes(rel) && existsSync(join(targetDir, rel))) written.push(rel);
|
|
173
|
+
// Drop removed locale files from the written list when English-only.
|
|
174
|
+
if (!existsSync(join(targetDir, rel))) {
|
|
175
|
+
const i = written.indexOf(rel);
|
|
176
|
+
if (i >= 0) written.splice(i, 1);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const pgdog = options.pgdog ?? createDefaults?.pgdog ?? false;
|
|
181
|
+
const configPath = join(targetDir, "oke.config.ts");
|
|
182
|
+
if (existsSync(configPath)) {
|
|
183
|
+
const prev = readFileSync(configPath, "utf8");
|
|
184
|
+
const next = applyPgDogToConfig(prev, pgdog);
|
|
185
|
+
if (next !== prev) writeFileSync(configPath, next, "utf8");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const composeFiles = await writeScaffoldCompose(targetDir);
|
|
189
|
+
for (const rel of composeFiles) {
|
|
190
|
+
if (!written.includes(rel)) written.push(rel);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const systemSchema = await writeSystemSchema(targetDir);
|
|
194
|
+
if (systemSchema && !written.includes(systemSchema)) written.push(systemSchema);
|
|
157
195
|
|
|
158
196
|
written.sort();
|
|
159
197
|
return {
|
|
@@ -164,6 +202,8 @@ export function scaffold(options: ScaffoldOptions): ScaffoldResult {
|
|
|
164
202
|
okengineDependency,
|
|
165
203
|
sqlDriver,
|
|
166
204
|
...(createDefaults !== undefined ? { createDefaults } : {}),
|
|
205
|
+
locales,
|
|
206
|
+
pgdog,
|
|
167
207
|
files: written,
|
|
168
208
|
};
|
|
169
209
|
} catch (e) {
|
|
@@ -173,12 +213,10 @@ export function scaffold(options: ScaffoldOptions): ScaffoldResult {
|
|
|
173
213
|
}
|
|
174
214
|
|
|
175
215
|
/**
|
|
176
|
-
* For postgres, pin `store.sql`
|
|
216
|
+
* For postgres, pin `store.sql` `dev`/`prod` in `oke.config.ts`.
|
|
177
217
|
*
|
|
178
|
-
* The default template ships
|
|
179
|
-
*
|
|
180
|
-
* `sqliteTable`/`pgTable` source to rewrite. `sqlite` keeps the template
|
|
181
|
-
* dual-mode config (`local: sqlite` · `docker`/`prod: postgres`) untouched.
|
|
218
|
+
* The default template already ships postgres for `dev`/`prod` — this is a
|
|
219
|
+
* no-op unless customize changed the SQL driver and `--sql postgres` restores it.
|
|
182
220
|
*
|
|
183
221
|
* @param targetDir - Scaffolded project root
|
|
184
222
|
* @param sqlDriver - Chosen store.sql driver
|
|
@@ -193,6 +231,38 @@ function applySqlDriverTransforms(targetDir: string, sqlDriver: SqlDriverId): vo
|
|
|
193
231
|
}
|
|
194
232
|
}
|
|
195
233
|
|
|
234
|
+
/** Marker that the built-in vault key note is already present. */
|
|
235
|
+
const VAULT_MASTER_KEY_MARKER = "OKE_VAULT_MASTER_KEY";
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Make sure `.env.example` explains the built-in vault's master key.
|
|
239
|
+
*
|
|
240
|
+
* Only the built-in `vault` backend has a key to hold: `env` reads the dotenv
|
|
241
|
+
* layers directly, and `managed` gets credentials from the provider secret
|
|
242
|
+
* store. The bundled templates already carry the note, so this only fires
|
|
243
|
+
* for a template that dropped it.
|
|
244
|
+
*
|
|
245
|
+
* @param targetDir - Scaffolded project root
|
|
246
|
+
* @param vaultDriver - Chosen `drivers.vault` dev pin
|
|
247
|
+
*/
|
|
248
|
+
function ensureVaultEnvNotes(targetDir: string, vaultDriver: string): void {
|
|
249
|
+
if (vaultDriver !== "vault") return;
|
|
250
|
+
const envExample = join(targetDir, ".env.example");
|
|
251
|
+
if (!existsSync(envExample)) return;
|
|
252
|
+
const source = readFileSync(envExample, "utf8");
|
|
253
|
+
if (source.includes(VAULT_MASTER_KEY_MARKER)) return;
|
|
254
|
+
writeFileSync(
|
|
255
|
+
envExample,
|
|
256
|
+
`${source.trimEnd()}\n
|
|
257
|
+
# ── vault — built-in encrypted-at-rest store ────────────────
|
|
258
|
+
# \`oke vault init\` prints the master key once; every later boot unseals with it.
|
|
259
|
+
# In production read it from your KMS instead of committing it here.
|
|
260
|
+
# OKE_VAULT_MASTER_KEY=
|
|
261
|
+
`,
|
|
262
|
+
"utf8",
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
196
266
|
/**
|
|
197
267
|
* Apply full customize / reuse defaults to `oke.config.ts`.
|
|
198
268
|
*
|
|
@@ -209,29 +279,203 @@ function applyCreateDefaultsTransforms(targetDir: string, defaults: CreateDefaul
|
|
|
209
279
|
}
|
|
210
280
|
|
|
211
281
|
/**
|
|
212
|
-
*
|
|
282
|
+
* Emit `.oke/schema/oke.ts` (system / auth / plugin stubs) when the local
|
|
283
|
+
* okengine source tree is available. Published create-oke regenerates after
|
|
284
|
+
* `bun install` via `oke schema generate`.
|
|
213
285
|
*
|
|
214
|
-
* @param
|
|
215
|
-
* @
|
|
286
|
+
* @param targetDir - Scaffolded project root
|
|
287
|
+
* @returns Relative path written, or `undefined` when skipped
|
|
216
288
|
*/
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
289
|
+
async function writeSystemSchema(targetDir: string): Promise<string | undefined> {
|
|
290
|
+
const okengineRoot = resolveLocalOkengineRoot();
|
|
291
|
+
if (!okengineRoot) return undefined;
|
|
292
|
+
try {
|
|
293
|
+
const schemaUrl = pathToFileURL(join(okengineRoot, "src/cli/schema.ts")).href;
|
|
294
|
+
const mod = (await import(schemaUrl)) as {
|
|
295
|
+
runSchemaGenerate: (opts: {
|
|
296
|
+
cwd?: string;
|
|
297
|
+
write?: (text: string) => void;
|
|
298
|
+
}) => Promise<number>;
|
|
299
|
+
SCHEMA_OUT: string;
|
|
300
|
+
};
|
|
301
|
+
const code = await mod.runSchemaGenerate({ cwd: targetDir, write: () => {} });
|
|
302
|
+
if (code !== 0) return undefined;
|
|
303
|
+
return mod.SCHEMA_OUT;
|
|
304
|
+
} catch {
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
224
307
|
}
|
|
225
308
|
|
|
226
309
|
/**
|
|
227
|
-
*
|
|
310
|
+
* Derive `docker/docker-compose.yml` via okengine's {@link deriveInfrastructure}
|
|
311
|
+
* when the monorepo root is available; otherwise write a minimal postgres+redis stub.
|
|
228
312
|
*
|
|
229
313
|
* @param targetDir - Scaffolded project root
|
|
230
|
-
* @
|
|
314
|
+
* @returns Relative docker paths written
|
|
231
315
|
*/
|
|
232
|
-
function
|
|
233
|
-
|
|
234
|
-
|
|
316
|
+
async function writeScaffoldCompose(targetDir: string): Promise<string[]> {
|
|
317
|
+
const configPath = join(targetDir, "oke.config.ts");
|
|
318
|
+
const images = existsSync(configPath)
|
|
319
|
+
? extractImages(readFileSync(configPath, "utf8"))
|
|
320
|
+
: { ...DEFAULT_IMAGES };
|
|
321
|
+
if (Object.keys(images).length === 0) {
|
|
322
|
+
Object.assign(images, DEFAULT_IMAGES);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const okengineRoot = resolveLocalOkengineRoot();
|
|
326
|
+
if (okengineRoot) {
|
|
327
|
+
try {
|
|
328
|
+
const deriveUrl = pathToFileURL(join(okengineRoot, "src/docker/derive.ts")).href;
|
|
329
|
+
const mod = (await import(deriveUrl)) as {
|
|
330
|
+
deriveInfrastructure: (opts: {
|
|
331
|
+
images: Readonly<Record<string, string>>;
|
|
332
|
+
app?: string;
|
|
333
|
+
composeDir?: string;
|
|
334
|
+
includeApp?: boolean;
|
|
335
|
+
prod?: boolean;
|
|
336
|
+
layout?: "single" | "split" | "stack";
|
|
337
|
+
}) => {
|
|
338
|
+
files: readonly { path: string; content: string }[];
|
|
339
|
+
};
|
|
340
|
+
writeDerivedFiles: (
|
|
341
|
+
result: { files: readonly { path: string; content: string }[] },
|
|
342
|
+
outDir: string,
|
|
343
|
+
options?: { writeStackEnv?: boolean },
|
|
344
|
+
) => Promise<readonly string[]>;
|
|
345
|
+
};
|
|
346
|
+
const result = mod.deriveInfrastructure({
|
|
347
|
+
images,
|
|
348
|
+
app: "app",
|
|
349
|
+
composeDir: "docker",
|
|
350
|
+
includeApp: true,
|
|
351
|
+
prod: true,
|
|
352
|
+
layout: "single",
|
|
353
|
+
});
|
|
354
|
+
const dockerDir = join(targetDir, "docker");
|
|
355
|
+
await mod.writeDerivedFiles(result, dockerDir, { writeStackEnv: false });
|
|
356
|
+
return result.files
|
|
357
|
+
.filter(
|
|
358
|
+
(f) => f.path.endsWith(".yml") || f.path === "Dockerfile" || f.path.endsWith(".toml"),
|
|
359
|
+
)
|
|
360
|
+
.map((f) => `docker/${f.path}`);
|
|
361
|
+
} catch {
|
|
362
|
+
// Fall through to stub when derive fails (published create-oke, etc.).
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return writeMinimalComposeStub(targetDir);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Minimal committed-style single-file compose with postgres + redis healthchecks.
|
|
371
|
+
*
|
|
372
|
+
* @param targetDir - Project root
|
|
373
|
+
*/
|
|
374
|
+
function writeMinimalComposeStub(targetDir: string): string[] {
|
|
375
|
+
const dockerDir = join(targetDir, "docker");
|
|
376
|
+
mkdirSync(dockerDir, { recursive: true });
|
|
377
|
+
const compose = `# Generated by create-oke (minimal stub when okengine derive is unavailable).
|
|
378
|
+
# Local overrides: docker-compose.override.yml (do not commit secrets).
|
|
379
|
+
|
|
380
|
+
name: oke-app
|
|
381
|
+
|
|
382
|
+
networks:
|
|
383
|
+
oke:
|
|
384
|
+
driver: bridge
|
|
385
|
+
|
|
386
|
+
services:
|
|
387
|
+
# App — okengine runtime
|
|
388
|
+
app:
|
|
389
|
+
image: oke-app:latest
|
|
390
|
+
build:
|
|
391
|
+
context: ..
|
|
392
|
+
dockerfile: Dockerfile
|
|
393
|
+
ports:
|
|
394
|
+
- "6530:6530"
|
|
395
|
+
env_file:
|
|
396
|
+
- .env.docker
|
|
397
|
+
depends_on:
|
|
398
|
+
store-sql:
|
|
399
|
+
condition: service_healthy
|
|
400
|
+
store-kv:
|
|
401
|
+
condition: service_healthy
|
|
402
|
+
networks:
|
|
403
|
+
- oke
|
|
404
|
+
healthcheck:
|
|
405
|
+
test:
|
|
406
|
+
- CMD
|
|
407
|
+
- bun
|
|
408
|
+
- -e
|
|
409
|
+
- fetch("http://127.0.0.1:6530/_/ready").then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))
|
|
410
|
+
interval: 10s
|
|
411
|
+
timeout: 3s
|
|
412
|
+
retries: 3
|
|
413
|
+
start_period: 60s
|
|
414
|
+
stop_grace_period: 30s
|
|
415
|
+
deploy:
|
|
416
|
+
replicas: 1
|
|
417
|
+
resources:
|
|
418
|
+
limits:
|
|
419
|
+
cpus: "0.72"
|
|
420
|
+
memory: 1475M
|
|
421
|
+
|
|
422
|
+
# store.sql — Postgres
|
|
423
|
+
store-sql:
|
|
424
|
+
image: postgres:18-alpine
|
|
425
|
+
ports:
|
|
426
|
+
- "127.0.0.1:5432:5432"
|
|
427
|
+
networks:
|
|
428
|
+
- oke
|
|
429
|
+
env_file:
|
|
430
|
+
- .env.docker
|
|
431
|
+
environment:
|
|
432
|
+
POSTGRES_USER: \${OKE_STORE_SQL_USER}
|
|
433
|
+
POSTGRES_PASSWORD: \${OKE_STORE_SQL_PASSWORD}
|
|
434
|
+
POSTGRES_DB: \${OKE_STORE_SQL_DB}
|
|
435
|
+
healthcheck:
|
|
436
|
+
test:
|
|
437
|
+
- CMD-SHELL
|
|
438
|
+
- pg_isready -U $$POSTGRES_USER
|
|
439
|
+
interval: 5s
|
|
440
|
+
timeout: 3s
|
|
441
|
+
retries: 10
|
|
442
|
+
deploy:
|
|
443
|
+
resources:
|
|
444
|
+
limits:
|
|
445
|
+
cpus: "1.08"
|
|
446
|
+
memory: 2212M
|
|
447
|
+
|
|
448
|
+
# store.kv — Redis
|
|
449
|
+
store-kv:
|
|
450
|
+
image: redis:8-alpine
|
|
451
|
+
ports:
|
|
452
|
+
- "127.0.0.1:6379:6379"
|
|
453
|
+
networks:
|
|
454
|
+
- oke
|
|
455
|
+
env_file:
|
|
456
|
+
- .env.docker
|
|
457
|
+
command:
|
|
458
|
+
- sh
|
|
459
|
+
- -c
|
|
460
|
+
- exec redis-server --requirepass "$$OKE_STORE_KV_PASSWORD"
|
|
461
|
+
healthcheck:
|
|
462
|
+
test:
|
|
463
|
+
- CMD
|
|
464
|
+
- redis-cli
|
|
465
|
+
- -a
|
|
466
|
+
- \${OKE_STORE_KV_PASSWORD}
|
|
467
|
+
- ping
|
|
468
|
+
interval: 5s
|
|
469
|
+
timeout: 3s
|
|
470
|
+
retries: 10
|
|
471
|
+
deploy:
|
|
472
|
+
resources:
|
|
473
|
+
limits:
|
|
474
|
+
cpus: "0.54"
|
|
475
|
+
memory: 1106M
|
|
476
|
+
`;
|
|
477
|
+
writeFileSync(join(dockerDir, "docker-compose.yml"), compose, "utf8");
|
|
478
|
+
return ["docker/docker-compose.yml"];
|
|
235
479
|
}
|
|
236
480
|
|
|
237
481
|
/**
|
package/src/templates.ts
CHANGED
|
@@ -17,14 +17,8 @@ export const DEFAULT_TEMPLATE: TemplateId = "standard";
|
|
|
17
17
|
|
|
18
18
|
/** One-line purpose for the starter (interactive select + help). */
|
|
19
19
|
export const TEMPLATE_PURPOSES: Readonly<Record<TemplateId, string>> = {
|
|
20
|
-
standard: "Notes app —
|
|
21
|
-
advanced: "Notes app —
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
/** Recommended `.oke/mode` when using recommended defaults (no customize). */
|
|
25
|
-
export const TEMPLATE_DEFAULT_MODE: Readonly<Record<TemplateId, "local" | "docker">> = {
|
|
26
|
-
standard: "local",
|
|
27
|
-
advanced: "docker",
|
|
20
|
+
standard: "Notes app — Docker-first (postgres · redis · s3 · smtp)",
|
|
21
|
+
advanced: "Notes app — Docker-first + store.index (meilisearch)",
|
|
28
22
|
};
|
|
29
23
|
|
|
30
24
|
/**
|
package/src/transform.test.ts
CHANGED
|
@@ -6,9 +6,9 @@ import { describe, expect, test } from "bun:test";
|
|
|
6
6
|
import { readFileSync } from "node:fs";
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { toCreateDefaults } from "./create-defaults.ts";
|
|
9
|
-
import { pinsDockerReady,
|
|
9
|
+
import { pinsDockerReady, recommendedDefaults, VAULT_CHOICES } from "./drivers-catalog.ts";
|
|
10
10
|
import { resolveTemplateDir } from "./templates.ts";
|
|
11
|
-
import { applyCreateAnswers, upsertAiDrivers } from "./transform.ts";
|
|
11
|
+
import { applyCreateAnswers, extractImages, upsertAiDrivers } from "./transform.ts";
|
|
12
12
|
import type { EnvDriverPins } from "./create-defaults.ts";
|
|
13
13
|
|
|
14
14
|
function templateConfig(id: "standard" | "advanced" = "advanced"): string {
|
|
@@ -34,67 +34,126 @@ function evalConfig(source: string): {
|
|
|
34
34
|
return new Function("defineConfig", body)(defineConfig) as ReturnType<typeof evalConfig>;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
function defaultsWithIndex(
|
|
37
|
+
function defaultsWithIndex(indexDev: string) {
|
|
38
38
|
return toCreateDefaults({
|
|
39
39
|
template: "advanced",
|
|
40
40
|
profile: "docker-ready",
|
|
41
41
|
drivers: {
|
|
42
42
|
store: {
|
|
43
|
-
sql: pinsDockerReady("
|
|
44
|
-
kv:
|
|
45
|
-
files:
|
|
46
|
-
index: pinsDockerReady(
|
|
43
|
+
sql: pinsDockerReady("postgres", "pglite"),
|
|
44
|
+
kv: pinsDockerReady("redis", "memory"),
|
|
45
|
+
files: pinsDockerReady("s3", "memory"),
|
|
46
|
+
index: pinsDockerReady(indexDev, "memory"),
|
|
47
47
|
},
|
|
48
|
-
signal:
|
|
49
|
-
clock:
|
|
50
|
-
vault:
|
|
51
|
-
channel: { email:
|
|
48
|
+
signal: pinsDockerReady("redis", "memory"),
|
|
49
|
+
clock: pinsDockerReady("postgres", "frozen"),
|
|
50
|
+
vault: pinsDockerReady("vault", "memory"),
|
|
51
|
+
channel: { email: pinsDockerReady("smtp", "console") },
|
|
52
52
|
ai: null,
|
|
53
53
|
},
|
|
54
54
|
ai: { enabled: false, provider: null, driver: null },
|
|
55
|
+
locales: [],
|
|
56
|
+
pgdog: false,
|
|
55
57
|
});
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
describe("applyCreateAnswers images", () => {
|
|
59
|
-
test("index:
|
|
60
|
-
const next = applyCreateAnswers(templateConfig(), defaultsWithIndex("
|
|
61
|
-
expect(next).toContain('
|
|
62
|
-
expect(next).toMatch(/index:\s*\{\s*
|
|
61
|
+
test("index: pgvector does not poison images with driver pins", () => {
|
|
62
|
+
const next = applyCreateAnswers(templateConfig(), defaultsWithIndex("pgvector"));
|
|
63
|
+
expect(next).toContain('dev: "pgvector"');
|
|
64
|
+
expect(next).toMatch(/index:\s*\{\s*dev: "pgvector"/);
|
|
63
65
|
// Role pins only — never env-column keys or bare driver ids as images.
|
|
64
|
-
expect(next).not.toMatch(/images:\s*\{[^}]*\
|
|
65
|
-
expect(next).not.toMatch(/images:\s*\{[^}]*\
|
|
66
|
-
expect(next).not.toMatch(/images:\s*\{[^}]*:\s*"
|
|
66
|
+
expect(next).not.toMatch(/images:\s*\{[^}]*\bdev:\s*"/s);
|
|
67
|
+
expect(next).not.toMatch(/images:\s*\{[^}]*\btest:\s*"memory"/s);
|
|
68
|
+
expect(next).not.toMatch(/images:\s*\{[^}]*:\s*"pgvector"/s);
|
|
67
69
|
expect(next).toMatch(/images:\s*\{\s*store:\s*\{[^}]*\bsql: "postgres:18-alpine"/s);
|
|
68
70
|
});
|
|
69
71
|
|
|
70
72
|
test("index meilisearch pins store.index image without comment leakage", () => {
|
|
71
|
-
const next = applyCreateAnswers(
|
|
72
|
-
templateConfig(),
|
|
73
|
-
defaultsWithIndex("meilisearch", "meilisearch"),
|
|
74
|
-
);
|
|
73
|
+
const next = applyCreateAnswers(templateConfig(), defaultsWithIndex("meilisearch"));
|
|
75
74
|
expect(next).toMatch(/images:\s*\{\s*store:\s*\{[^}]*\bindex: "getmeili\/meilisearch:v1.37"/s);
|
|
76
75
|
expect(next).not.toMatch(/images:\s*\{[^}]*\btest:\s*"memory"/s);
|
|
77
76
|
});
|
|
78
77
|
});
|
|
79
78
|
|
|
79
|
+
describe("vault backend defaults", () => {
|
|
80
|
+
test("recommended defaults pick the built-in encrypted store", () => {
|
|
81
|
+
const defaults = recommendedDefaults("docker-ready", "standard");
|
|
82
|
+
expect(defaults.drivers.vault).toEqual({ dev: "vault", test: "memory", prod: "vault" });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("the wizard offers env, vault, managed, and memory", () => {
|
|
86
|
+
const values = VAULT_CHOICES.map((c) => c.value);
|
|
87
|
+
expect(values).toEqual(["env", "vault", "managed", "memory"]);
|
|
88
|
+
expect(VAULT_CHOICES.find((c) => c.value === "vault")?.label).toContain("recommended");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("both templates pin only vault.dev (built-in) — other drivers use defaults", () => {
|
|
92
|
+
for (const id of ["standard", "advanced"] as const) {
|
|
93
|
+
const source = templateConfig(id);
|
|
94
|
+
expect(source, id).toMatch(/vault:\s*\{\s*dev: "vault",?\s*\}/);
|
|
95
|
+
expect(source, id).not.toMatch(/^\s*sql:\s*\{/m);
|
|
96
|
+
expect(source, id).not.toMatch(/^\s*signal:\s*\{/m);
|
|
97
|
+
expect(extractImages(source).vault, id).toBeUndefined();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("both templates document the master key in .env.example", () => {
|
|
102
|
+
for (const id of ["standard", "advanced"] as const) {
|
|
103
|
+
const env = readFileSync(join(resolveTemplateDir(id), ".env.example"), "utf8");
|
|
104
|
+
expect(env, id).toContain("OKE_VAULT_MASTER_KEY");
|
|
105
|
+
expect(env, id).toContain("oke vault init");
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("managed vault does not pin a compose image", () => {
|
|
110
|
+
const next = applyCreateAnswers(
|
|
111
|
+
templateConfig("standard"),
|
|
112
|
+
toCreateDefaults({
|
|
113
|
+
template: "standard",
|
|
114
|
+
profile: "docker-ready",
|
|
115
|
+
drivers: {
|
|
116
|
+
store: {
|
|
117
|
+
sql: pinsDockerReady("postgres", "pglite"),
|
|
118
|
+
kv: pinsDockerReady("redis", "memory"),
|
|
119
|
+
files: pinsDockerReady("s3", "memory"),
|
|
120
|
+
index: null,
|
|
121
|
+
},
|
|
122
|
+
signal: pinsDockerReady("redis", "memory"),
|
|
123
|
+
clock: pinsDockerReady("postgres", "frozen"),
|
|
124
|
+
vault: pinsDockerReady("managed", "memory"),
|
|
125
|
+
channel: { email: pinsDockerReady("smtp", "console") },
|
|
126
|
+
ai: null,
|
|
127
|
+
},
|
|
128
|
+
ai: { enabled: false, provider: null, driver: null },
|
|
129
|
+
locales: [],
|
|
130
|
+
pgdog: false,
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
expect(next).toMatch(/vault:\s*\{\s*dev: "managed"/);
|
|
134
|
+
expect(extractImages(next).vault).toBeUndefined();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
80
138
|
describe("upsertAiDrivers", () => {
|
|
81
139
|
const ollamaPins: EnvDriverPins = {
|
|
82
|
-
|
|
83
|
-
docker: "ollama",
|
|
140
|
+
dev: "ollama",
|
|
84
141
|
test: "mock",
|
|
85
142
|
prod: "ollama",
|
|
86
143
|
};
|
|
87
144
|
|
|
88
|
-
test("inserts drivers.ai
|
|
145
|
+
test("inserts drivers.ai inside drivers (not images / channel)", () => {
|
|
89
146
|
const next = upsertAiDrivers(templateConfig("advanced"), ollamaPins);
|
|
90
147
|
const config = evalConfig(next);
|
|
91
148
|
expect(config.drivers?.ai).toEqual(ollamaPins);
|
|
92
149
|
expect(config.drivers?.channel?.ai).toBeUndefined();
|
|
93
|
-
|
|
150
|
+
// Sparse templates omit drivers.channel — images.channel stays a string pin.
|
|
151
|
+
expect(config.images?.channel?.email).toBe("axllent/mailpit:v1.22.3");
|
|
152
|
+
expect(config.images?.ai).toBeUndefined();
|
|
94
153
|
});
|
|
95
154
|
|
|
96
155
|
test("applyCreateAnswers with ai pins keeps top-level drivers.ai", () => {
|
|
97
|
-
const llamaPins = pinsDockerReady("openai-compatible", "
|
|
156
|
+
const llamaPins = pinsDockerReady("openai-compatible", "mock");
|
|
98
157
|
for (const id of ["standard", "advanced"] as const) {
|
|
99
158
|
const next = applyCreateAnswers(
|
|
100
159
|
templateConfig(id),
|
|
@@ -103,24 +162,25 @@ describe("upsertAiDrivers", () => {
|
|
|
103
162
|
profile: "docker-ready",
|
|
104
163
|
drivers: {
|
|
105
164
|
store: {
|
|
106
|
-
sql: pinsDockerReady("
|
|
107
|
-
kv:
|
|
108
|
-
files:
|
|
165
|
+
sql: pinsDockerReady("postgres", "pglite"),
|
|
166
|
+
kv: pinsDockerReady("redis", "memory"),
|
|
167
|
+
files: pinsDockerReady("s3", "memory"),
|
|
109
168
|
index: null,
|
|
110
169
|
},
|
|
111
|
-
signal:
|
|
112
|
-
clock:
|
|
113
|
-
vault:
|
|
114
|
-
channel: { email:
|
|
170
|
+
signal: pinsDockerReady("redis", "memory"),
|
|
171
|
+
clock: pinsDockerReady("postgres", "frozen"),
|
|
172
|
+
vault: pinsDockerReady("vault", "memory"),
|
|
173
|
+
channel: { email: pinsDockerReady("smtp", "console") },
|
|
115
174
|
ai: llamaPins,
|
|
116
175
|
},
|
|
117
176
|
ai: { enabled: true, provider: "llama-cpp", driver: "openai-compatible" },
|
|
177
|
+
locales: [],
|
|
178
|
+
pgdog: false,
|
|
118
179
|
}),
|
|
119
180
|
);
|
|
120
181
|
const config = evalConfig(next);
|
|
121
182
|
expect(config.drivers?.ai, id).toEqual({
|
|
122
|
-
|
|
123
|
-
docker: "openai-compatible",
|
|
183
|
+
dev: "openai-compatible",
|
|
124
184
|
test: "mock",
|
|
125
185
|
prod: "openai-compatible",
|
|
126
186
|
});
|