@cosmicdrift/kumiko-dev-server 0.363.0 → 0.365.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/dist/build-server-bundle.js +12 -9
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +0 -1
- package/dist/compose-stacks.d.ts +8 -1
- package/dist/compose-stacks.js +6 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/resolve-dev-pii-kms.d.ts +2 -0
- package/dist/resolve-dev-pii-kms.js +16 -3
- package/dist/run-dev-app.js +16 -8
- package/dist/scaffold-agent-docs.d.ts +3 -0
- package/dist/scaffold-agent-docs.js +17 -0
- package/dist/scaffold-app-feature.js +1 -2
- package/dist/scaffold-app.d.ts +1 -0
- package/dist/scaffold-app.js +275 -295
- package/dist/scaffold-demo-tasks.js +2 -1
- package/package.json +8 -9
- package/src/changes.json +43 -0
- package/bin/kumiko-schema-check.ts +0 -175
- package/dist/schema-check-core.d.ts +0 -2
- package/dist/schema-check-core.js +0 -24
package/dist/scaffold-app.js
CHANGED
|
@@ -6,18 +6,28 @@
|
|
|
6
6
|
// stub, package.json with @cosmicdrift/* deps, tsconfig, .env.example,
|
|
7
7
|
// README.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// generate + later modify — no template-string ↔ ts-morph divergence.
|
|
9
|
+
// Generated files are emitted already formatted to Biome's output (import
|
|
10
|
+
// order, 100-col wrapping, trailing commas) so a fresh app passes `biome check`.
|
|
12
11
|
// Static files (package.json, tsconfig, .env, README) stay text-based.
|
|
13
12
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
14
13
|
import { dirname, join, relative, resolve } from "node:path";
|
|
15
14
|
import { collectTableMetas, generateMigration, writeSnapshotJson, } from "@cosmicdrift/kumiko-framework/db";
|
|
16
15
|
import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";
|
|
17
|
-
import { IndentationText, Project, VariableDeclarationKind } from "ts-morph";
|
|
18
16
|
import { isKebabSegment } from "./kebab.js";
|
|
17
|
+
import { renderAgentsMd, renderClaudeMd } from "./scaffold-agent-docs.js";
|
|
19
18
|
import { createDemoTasksFeature, renderDemoSeedFile, renderDemoTasksFeatureFile, renderDemoTasksI18n, renderDemoTasksIndex, renderDemoTasksWebIndex, } from "./scaffold-demo-tasks.js";
|
|
20
19
|
import { scaffoldDeploy } from "./scaffold-deploy.js";
|
|
20
|
+
// Exact pin (no caret): the generated files are formatted to this Biome's output,
|
|
21
|
+
// and a scaffold-lint test keeps it equal to the monorepo's own Biome version.
|
|
22
|
+
export const SCAFFOLD_BIOME_VERSION = "2.5.7";
|
|
23
|
+
const SCAFFOLD_GITIGNORE = `node_modules/
|
|
24
|
+
dist/
|
|
25
|
+
dist-server/
|
|
26
|
+
.env
|
|
27
|
+
.kumiko/
|
|
28
|
+
test-results/
|
|
29
|
+
playwright-report/
|
|
30
|
+
`;
|
|
21
31
|
export async function scaffoldApp(options) {
|
|
22
32
|
if (!isKebabSegment(options.name)) {
|
|
23
33
|
throw new Error(`scaffoldApp: name must be kebab-case (a-z, 0-9, -); got "${options.name}"`);
|
|
@@ -41,6 +51,8 @@ export async function scaffoldApp(options) {
|
|
|
41
51
|
files.push("kumiko.json");
|
|
42
52
|
write(join(destination, "biome.json"), renderBiomeJson());
|
|
43
53
|
files.push("biome.json");
|
|
54
|
+
write(join(destination, ".gitignore"), SCAFFOLD_GITIGNORE);
|
|
55
|
+
files.push(".gitignore");
|
|
44
56
|
if (testSetup === undefined) {
|
|
45
57
|
write(join(destination, "bunfig.toml"), renderBunfigToml());
|
|
46
58
|
files.push("bunfig.toml");
|
|
@@ -93,6 +105,10 @@ export async function scaffoldApp(options) {
|
|
|
93
105
|
}
|
|
94
106
|
write(join(destination, "README.md"), `${renderReadme(options.name, options.features)}${testSetup === undefined ? "" : `\n${testSetup.rulesMarkdown}`}`);
|
|
95
107
|
files.push("README.md");
|
|
108
|
+
write(join(destination, "AGENTS.md"), renderAgentsMd());
|
|
109
|
+
files.push("AGENTS.md");
|
|
110
|
+
write(join(destination, "CLAUDE.md"), renderClaudeMd());
|
|
111
|
+
files.push("CLAUDE.md");
|
|
96
112
|
return { destination, files, appName: options.name };
|
|
97
113
|
}
|
|
98
114
|
// @wrapper-known semantic-alias
|
|
@@ -127,7 +143,7 @@ function renderPackageJson(name, version, testSetup) {
|
|
|
127
143
|
zod: "^4.4.3",
|
|
128
144
|
},
|
|
129
145
|
devDependencies: sortedByName({
|
|
130
|
-
"@biomejs/biome":
|
|
146
|
+
"@biomejs/biome": SCAFFOLD_BIOME_VERSION,
|
|
131
147
|
"@tailwindcss/cli": "^4.3.0",
|
|
132
148
|
"@types/react": "^19.2.0",
|
|
133
149
|
"@types/react-dom": "^19.2.0",
|
|
@@ -163,7 +179,7 @@ function renderTsconfig() {
|
|
|
163
179
|
}
|
|
164
180
|
function renderBiomeJson() {
|
|
165
181
|
return `${JSON.stringify({
|
|
166
|
-
$schema:
|
|
182
|
+
$schema: `https://biomejs.dev/schemas/${SCAFFOLD_BIOME_VERSION}/schema.json`,
|
|
167
183
|
vcs: {
|
|
168
184
|
enabled: true,
|
|
169
185
|
clientKind: "git",
|
|
@@ -204,7 +220,7 @@ function renderBiomeJson() {
|
|
|
204
220
|
linter: {
|
|
205
221
|
enabled: true,
|
|
206
222
|
rules: {
|
|
207
|
-
|
|
223
|
+
preset: "recommended",
|
|
208
224
|
correctness: { noUnusedVariables: "error", noUnusedImports: "error" },
|
|
209
225
|
suspicious: { noExplicitAny: "error", noDebugger: "error", noConsole: "warn" },
|
|
210
226
|
complexity: { useLiteralKeys: "off" },
|
|
@@ -249,13 +265,6 @@ pathIgnorePatterns = [
|
|
|
249
265
|
]
|
|
250
266
|
`;
|
|
251
267
|
}
|
|
252
|
-
function newTsProject() {
|
|
253
|
-
return new Project({
|
|
254
|
-
useInMemoryFileSystem: true,
|
|
255
|
-
compilerOptions: { target: 99, module: 99, strict: true },
|
|
256
|
-
manipulationSettings: { indentationText: IndentationText.TwoSpaces },
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
268
|
const FOUNDATION_FEATURES = [
|
|
260
269
|
{
|
|
261
270
|
name: "secrets",
|
|
@@ -284,9 +293,40 @@ const FOUNDATION_FEATURES = [
|
|
|
284
293
|
// jemand scaffoldApp() direkt mit allen Bundled-Feature-Entries aufruft
|
|
285
294
|
// rutschen die 4 nicht in run-config.ts.
|
|
286
295
|
const COMPOSE_AUTO_MOUNTED_NAMES = new Set(["config", "user", "tenant", "auth-email-password"]);
|
|
296
|
+
const MAX_LINE_WIDTH = 100;
|
|
297
|
+
// Mirrors Biome's import sort so generated files pass `biome check` untouched:
|
|
298
|
+
// packages alphabetically by specifier, relative paths last.
|
|
299
|
+
function compareImportKeys(a, b) {
|
|
300
|
+
const lowerA = a.replace(/^type /, "").toLowerCase();
|
|
301
|
+
const lowerB = b.replace(/^type /, "").toLowerCase();
|
|
302
|
+
if (lowerA !== lowerB)
|
|
303
|
+
return lowerA < lowerB ? -1 : 1;
|
|
304
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
305
|
+
}
|
|
306
|
+
function isRelativeSpecifier(specifier) {
|
|
307
|
+
return specifier.startsWith("./") || specifier.startsWith("../");
|
|
308
|
+
}
|
|
309
|
+
function renderImportLine({ specifier, names, typeOnly }) {
|
|
310
|
+
const sorted = [...names].sort(compareImportKeys);
|
|
311
|
+
const keyword = typeOnly === true ? "import type" : "import";
|
|
312
|
+
const oneLine = `${keyword} { ${sorted.join(", ")} } from "${specifier}";`;
|
|
313
|
+
if (sorted.length === 1 || oneLine.length <= MAX_LINE_WIDTH)
|
|
314
|
+
return oneLine;
|
|
315
|
+
return `${keyword} {\n${sorted.map((n) => ` ${n},`).join("\n")}\n} from "${specifier}";`;
|
|
316
|
+
}
|
|
317
|
+
function renderSortedImports(imports) {
|
|
318
|
+
return [...imports]
|
|
319
|
+
.sort((a, b) => {
|
|
320
|
+
const relativeA = isRelativeSpecifier(a.specifier);
|
|
321
|
+
const relativeB = isRelativeSpecifier(b.specifier);
|
|
322
|
+
if (relativeA !== relativeB)
|
|
323
|
+
return relativeA ? 1 : -1;
|
|
324
|
+
return compareImportKeys(a.specifier, b.specifier);
|
|
325
|
+
})
|
|
326
|
+
.map(renderImportLine)
|
|
327
|
+
.join("\n");
|
|
328
|
+
}
|
|
287
329
|
function renderRunConfig(features) {
|
|
288
|
-
const project = newTsProject();
|
|
289
|
-
const sf = project.createSourceFile("run-config.ts", "");
|
|
290
330
|
// Fallback decided BEFORE filtering: if the caller passed features (even
|
|
291
331
|
// if every one of them is an auto-mounted name), an all-filtered-out empty
|
|
292
332
|
// result is the correct outcome — falling back to FOUNDATION_FEATURES here
|
|
@@ -301,289 +341,215 @@ function renderRunConfig(features) {
|
|
|
301
341
|
existing.push(entry.exportName);
|
|
302
342
|
grouped.set(entry.importPath, existing);
|
|
303
343
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
moduleSpecifier: "./features/tasks",
|
|
309
|
-
namedImports: ["tasksFeature"],
|
|
310
|
-
});
|
|
344
|
+
const imports = [
|
|
345
|
+
...[...grouped].map(([specifier, names]) => ({ specifier, names })),
|
|
346
|
+
{ specifier: "./features/tasks", names: ["tasksFeature"] },
|
|
347
|
+
];
|
|
311
348
|
const callExprs = [...effective.map((f) => f.callExpression), "tasksFeature"];
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
349
|
+
const oneLineFeatures = `export const APP_FEATURES = [${callExprs.join(", ")}] as const;`;
|
|
350
|
+
const appFeatures = oneLineFeatures.length <= MAX_LINE_WIDTH
|
|
351
|
+
? oneLineFeatures
|
|
352
|
+
: `export const APP_FEATURES = [\n${callExprs.map((c) => ` ${c},`).join("\n")}\n] as const;`;
|
|
353
|
+
return `// Single source of truth for your app's feature composition.
|
|
354
|
+
// config/user/tenant/auth-email-password are added automatically
|
|
355
|
+
// via composeFeatures(includeBundled:true) when runProdApp is called
|
|
356
|
+
// with \`auth: {…}\` (see bin/main.ts).
|
|
357
|
+
//
|
|
358
|
+
// Add new features:
|
|
359
|
+
// - bunx @cosmicdrift/kumiko-cli add feature <name> (DX-2, automatic)
|
|
360
|
+
// - or: hand-edit + add the import below
|
|
361
|
+
|
|
362
|
+
${renderSortedImports(imports)}
|
|
363
|
+
|
|
364
|
+
${appFeatures}
|
|
365
|
+
|
|
366
|
+
export const HAS_AUTH = true;
|
|
367
|
+
`;
|
|
368
|
+
}
|
|
369
|
+
function renderAdminAuthBlock(appName, extraAdminLines) {
|
|
370
|
+
const passwordAndExtras = extraAdminLines.map((l) => ` ${l}`).join("\n");
|
|
371
|
+
return ` auth: {
|
|
372
|
+
admin: {
|
|
373
|
+
email: "admin@${appName}.local",
|
|
374
|
+
${passwordAndExtras}
|
|
375
|
+
memberships: [
|
|
376
|
+
{
|
|
377
|
+
tenantId: DEFAULT_TENANT_ID,
|
|
378
|
+
tenantKey: "${appName}",
|
|
379
|
+
tenantName: "${appName}",
|
|
380
|
+
roles: ["TenantAdmin"],
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
},
|
|
384
|
+
},`;
|
|
340
385
|
}
|
|
341
386
|
function renderMain(appName) {
|
|
342
387
|
const tenantId = deriveTenantId(appName);
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
{
|
|
411
|
-
name: "kmsOpts",
|
|
412
|
-
initializer: (writer) => {
|
|
413
|
-
writer.write("{").newLine();
|
|
414
|
-
writer.writeLine(` logPrefix: "[${appName}]",`);
|
|
415
|
-
writer.writeLine(` plaintextReason: "no PLATFORM_KEK / SUBJECT_KEYS_DATABASE_URL / KUMIKO_BLIND_INDEX_KEY set — see .env.example",`);
|
|
416
|
-
writer.write("} as const");
|
|
417
|
-
},
|
|
418
|
-
},
|
|
419
|
-
],
|
|
420
|
-
});
|
|
421
|
-
sf.addVariableStatement({
|
|
422
|
-
declarationKind: VariableDeclarationKind.Const,
|
|
423
|
-
declarations: [
|
|
424
|
-
{
|
|
425
|
-
name: "kmsWiring",
|
|
426
|
-
initializer: (writer) => {
|
|
427
|
-
writer.write(`process.env["NODE_ENV"] === "production"`);
|
|
428
|
-
writer.newLine();
|
|
429
|
-
writer.write(" ? requireKmsWiring(process.env, kmsOpts)");
|
|
430
|
-
writer.newLine();
|
|
431
|
-
writer.write(" : resolveKmsWiring(process.env, kmsOpts)");
|
|
432
|
-
},
|
|
433
|
-
},
|
|
434
|
-
],
|
|
435
|
-
});
|
|
436
|
-
sf.addStatements((writer) => {
|
|
437
|
-
writer
|
|
438
|
-
.write("await runProdApp(")
|
|
439
|
-
.inlineBlock(() => {
|
|
440
|
-
writer.writeLine("features: APP_FEATURES,");
|
|
441
|
-
writer.writeLine("envSchema,");
|
|
442
|
-
writer.writeLine('staticDir: "./dist",');
|
|
443
|
-
writer.writeLine("...kmsWiring,");
|
|
444
|
-
writer.write("auth: ").inlineBlock(() => {
|
|
445
|
-
writer.write("admin: ").inlineBlock(() => {
|
|
446
|
-
writer.writeLine(`email: "admin@${appName}.local",`);
|
|
447
|
-
writer.writeLine(`password: "change-me-on-first-deploy",`);
|
|
448
|
-
writer.writeLine(`displayName: "Admin",`);
|
|
449
|
-
writer.write("memberships: [");
|
|
450
|
-
writer.indent(() => {
|
|
451
|
-
writer.inlineBlock(() => {
|
|
452
|
-
writer.writeLine("tenantId: DEFAULT_TENANT_ID,");
|
|
453
|
-
writer.writeLine(`tenantKey: "${appName}",`);
|
|
454
|
-
writer.writeLine(`tenantName: "${appName}",`);
|
|
455
|
-
writer.writeLine(`roles: ["TenantAdmin"],`);
|
|
456
|
-
});
|
|
457
|
-
writer.write(",");
|
|
458
|
-
});
|
|
459
|
-
writer.write("],");
|
|
460
|
-
});
|
|
461
|
-
});
|
|
462
|
-
})
|
|
463
|
-
.write(");");
|
|
464
|
-
});
|
|
465
|
-
sf.insertText(0, [
|
|
466
|
-
"// Production-bootstrap. KUMIKO_DRY_RUN_ENV=boot exits after",
|
|
467
|
-
"// composeFeatures + validateBoot + createRegistry without DB/Redis-connect",
|
|
468
|
-
"// (see @cosmicdrift/kumiko-server-runtime runProdApp). The real dev boot",
|
|
469
|
-
"// runs via `bunx kumiko dev` (in-repo dev-tool) with a Docker stack — DX-1.0",
|
|
470
|
-
"// only covers the boot-mode path; `kumiko dev` lands in a later DX phase.",
|
|
471
|
-
"",
|
|
472
|
-
"",
|
|
473
|
-
].join("\n"));
|
|
474
|
-
return sf.getFullText();
|
|
388
|
+
const imports = renderSortedImports([
|
|
389
|
+
{ specifier: "@cosmicdrift/kumiko-dev-server/env-schema", names: ["frameworkCoreEnvSchema"] },
|
|
390
|
+
{ specifier: "@cosmicdrift/kumiko-server-runtime", names: ["composeFeatures", "runProdApp"] },
|
|
391
|
+
{ specifier: "@cosmicdrift/kumiko-framework/engine", names: ["TenantId"], typeOnly: true },
|
|
392
|
+
{ specifier: "@cosmicdrift/kumiko-framework/env", names: ["composeEnvSchema"] },
|
|
393
|
+
{
|
|
394
|
+
specifier: "@cosmicdrift/kumiko-framework/crypto",
|
|
395
|
+
names: ["requireKmsWiring", "resolveKmsWiring"],
|
|
396
|
+
},
|
|
397
|
+
{ specifier: "zod", names: ["z"] },
|
|
398
|
+
{ specifier: "../src/run-config", names: ["APP_FEATURES", "HAS_AUTH"] },
|
|
399
|
+
]);
|
|
400
|
+
const authBlock = renderAdminAuthBlock(appName, [
|
|
401
|
+
"// runProdApp validates envSchema before seeding: a missing or short value aborts the boot.",
|
|
402
|
+
'password: process.env["DEMO_ADMIN_PASSWORD"] ?? "",',
|
|
403
|
+
'displayName: "Admin",',
|
|
404
|
+
]);
|
|
405
|
+
// The envSchema must cover the SAME features runProdApp mounts at boot:
|
|
406
|
+
// `auth: { admin: … }` makes runProdApp auto-mix config/user/tenant/
|
|
407
|
+
// auth-email-password, so the JWT_SECRET (min-32) declaration has to be part
|
|
408
|
+
// of the boot-gate here too. Subject-key KMS is optional locally (plaintext +
|
|
409
|
+
// loud warning) but required under NODE_ENV=production (fw#2317 / #2339).
|
|
410
|
+
return `// Production-bootstrap. KUMIKO_DRY_RUN_ENV=boot exits after
|
|
411
|
+
// composeFeatures + validateBoot + createRegistry without DB/Redis-connect
|
|
412
|
+
// (see @cosmicdrift/kumiko-server-runtime runProdApp). The real dev boot
|
|
413
|
+
// runs via \`bunx kumiko dev\` (in-repo dev-tool) with a Docker stack — DX-1.0
|
|
414
|
+
// only covers the boot-mode path; \`kumiko dev\` lands in a later DX phase.
|
|
415
|
+
|
|
416
|
+
${imports}
|
|
417
|
+
|
|
418
|
+
const DEFAULT_TENANT_ID = "${tenantId}" as TenantId;
|
|
419
|
+
|
|
420
|
+
const bootFeatures = composeFeatures(APP_FEATURES, { includeBundled: HAS_AUTH });
|
|
421
|
+
|
|
422
|
+
const envSchema = composeEnvSchema({
|
|
423
|
+
core: frameworkCoreEnvSchema,
|
|
424
|
+
features: bootFeatures,
|
|
425
|
+
extend: z.object({
|
|
426
|
+
DEMO_ADMIN_PASSWORD: z
|
|
427
|
+
.string()
|
|
428
|
+
.min(12)
|
|
429
|
+
.describe(
|
|
430
|
+
"Password of the admin seeded on first boot (admin@${appName}.local). Generate with: openssl rand -hex 24",
|
|
431
|
+
)
|
|
432
|
+
.meta({ kumiko: { pulumi: { secret: true } } }),
|
|
433
|
+
}),
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const kmsOpts = {
|
|
437
|
+
logPrefix: "[${appName}]",
|
|
438
|
+
plaintextReason:
|
|
439
|
+
"no PLATFORM_KEK / SUBJECT_KEYS_DATABASE_URL / KUMIKO_BLIND_INDEX_KEY set — see .env.example",
|
|
440
|
+
} as const;
|
|
441
|
+
|
|
442
|
+
const kmsWiring =
|
|
443
|
+
process.env["NODE_ENV"] === "production"
|
|
444
|
+
? requireKmsWiring(process.env, kmsOpts)
|
|
445
|
+
: resolveKmsWiring(process.env, kmsOpts);
|
|
446
|
+
|
|
447
|
+
await runProdApp({
|
|
448
|
+
features: APP_FEATURES,
|
|
449
|
+
envSchema,
|
|
450
|
+
staticDir: "./dist",
|
|
451
|
+
...kmsWiring,
|
|
452
|
+
${authBlock}
|
|
453
|
+
});
|
|
454
|
+
`;
|
|
475
455
|
}
|
|
476
456
|
function renderDev(appName) {
|
|
477
457
|
const tenantId = deriveTenantId(appName);
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
});
|
|
531
|
-
writer.write(",");
|
|
532
|
-
});
|
|
533
|
-
writer.write("],");
|
|
534
|
-
});
|
|
535
|
-
});
|
|
536
|
-
})
|
|
537
|
-
.write(");");
|
|
538
|
-
});
|
|
539
|
-
sf.insertText(0, [
|
|
540
|
-
"// Dev bootstrap. `bun --watch bin/dev.ts` (see package.json scripts.dev)",
|
|
541
|
-
"// starts a full-featured dev server with auto-reload on code changes.",
|
|
542
|
-
"// setupTestStack creates missing entity tables automatically — a new",
|
|
543
|
-
"// r.entity(...) in a feature becomes a CREATE TABLE on the next reboot,",
|
|
544
|
-
"// no manual `kumiko schema apply` needed (that's prod-only).",
|
|
545
|
-
"// Persistent DB via KUMIKO_DEV_DB_NAME (.env) so admin + data survive reboots.",
|
|
546
|
-
"",
|
|
547
|
-
"",
|
|
548
|
-
].join("\n"));
|
|
549
|
-
return sf.getFullText();
|
|
458
|
+
const imports = renderSortedImports([
|
|
459
|
+
{ specifier: "@cosmicdrift/kumiko-dev-server", names: ["runDevApp"] },
|
|
460
|
+
{ specifier: "@cosmicdrift/kumiko-framework/engine", names: ["TenantId"], typeOnly: true },
|
|
461
|
+
{ specifier: "../src/run-config", names: ["APP_FEATURES"] },
|
|
462
|
+
{ specifier: "../src/seed", names: ["seedDemoTasks"] },
|
|
463
|
+
]);
|
|
464
|
+
// Dev-only emailVerified skips the login verify gate for the seeded admin;
|
|
465
|
+
// prod's bin/main.ts leaves it unset because the verify flow must run there.
|
|
466
|
+
const authBlock = renderAdminAuthBlock(appName, [
|
|
467
|
+
'password: "changeme",',
|
|
468
|
+
'displayName: "Admin",',
|
|
469
|
+
"emailVerified: true,",
|
|
470
|
+
]);
|
|
471
|
+
return `// Dev bootstrap. \`bun --watch bin/dev.ts\` (see package.json scripts.dev)
|
|
472
|
+
// starts a full-featured dev server with auto-reload on code changes.
|
|
473
|
+
// setupTestStack creates missing entity tables automatically — a new
|
|
474
|
+
// r.entity(...) in a feature becomes a CREATE TABLE on the next reboot,
|
|
475
|
+
// no manual \`kumiko schema apply\` needed (that's prod-only).
|
|
476
|
+
// Persistent DB via KUMIKO_DEV_DB_NAME (.env) so admin + data survive reboots.
|
|
477
|
+
|
|
478
|
+
${imports}
|
|
479
|
+
|
|
480
|
+
const DEFAULT_TENANT_ID = "${tenantId}" as TenantId;
|
|
481
|
+
|
|
482
|
+
await runDevApp({
|
|
483
|
+
features: APP_FEATURES,
|
|
484
|
+
welcomeBanner: true,
|
|
485
|
+
clientEntry: "./src/client.tsx",
|
|
486
|
+
seeds: [seedDemoTasks],
|
|
487
|
+
${authBlock}
|
|
488
|
+
});
|
|
489
|
+
`;
|
|
490
|
+
}
|
|
491
|
+
// Tiers mirror how Biome wraps the JSX as the name grows.
|
|
492
|
+
function renderDefaultAppShellElement(appName) {
|
|
493
|
+
const spanOpen = '<span className="font-semibold tracking-tight">';
|
|
494
|
+
// html-ok: appName is kebab-validated (a-z/0-9/-); this is generated .tsx source, not runtime HTML.
|
|
495
|
+
const brandInline = `${spanOpen}${appName}</span>`;
|
|
496
|
+
const singleLine = ` <DefaultAppShell brand={${brandInline}} schema={schema}>`;
|
|
497
|
+
if (singleLine.length <= MAX_LINE_WIDTH)
|
|
498
|
+
return singleLine;
|
|
499
|
+
const brandAttrInline = ` brand={${brandInline}}`;
|
|
500
|
+
const brandAttribute = brandAttrInline.length <= MAX_LINE_WIDTH
|
|
501
|
+
? brandAttrInline
|
|
502
|
+
: `${[
|
|
503
|
+
" brand={",
|
|
504
|
+
...(` ${brandInline}`.length <= MAX_LINE_WIDTH
|
|
505
|
+
? [` ${brandInline}`]
|
|
506
|
+
: [` ${spanOpen}`, ` ${appName}`, " </span>"]),
|
|
507
|
+
" }",
|
|
508
|
+
].join("\n")}`;
|
|
509
|
+
return [" <DefaultAppShell", brandAttribute, " schema={schema}", " >"].join("\n");
|
|
550
510
|
}
|
|
551
511
|
function renderClient(appName) {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
"
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
512
|
+
const imports = renderSortedImports([
|
|
513
|
+
{
|
|
514
|
+
specifier: "@cosmicdrift/kumiko-bundled-features/auth-email-password/web",
|
|
515
|
+
names: ["emailPasswordClient"],
|
|
516
|
+
},
|
|
517
|
+
{ specifier: "./features/tasks/web", names: ["tasksClient"] },
|
|
518
|
+
{
|
|
519
|
+
specifier: "@cosmicdrift/kumiko-renderer-web",
|
|
520
|
+
names: ["type AppSchema", "createKumikoApp", "DefaultAppShell"],
|
|
521
|
+
},
|
|
522
|
+
{ specifier: "react", names: ["ReactNode"], typeOnly: true },
|
|
523
|
+
]);
|
|
524
|
+
return `// Browser entry. runDevApp's clientEntry option bundles this file to
|
|
525
|
+
// /client.js and the default HTML loads it. createKumikoApp fetches the
|
|
526
|
+
// AppSchema itself from the authenticated GET /api/schema and mounts
|
|
527
|
+
// the routes.
|
|
528
|
+
//
|
|
529
|
+
// AppShell wraps DefaultAppShell to supply \`brand\` — createKumikoApp's
|
|
530
|
+
// shell option only injects schema + children. Without \`shell\` the
|
|
531
|
+
// active screen renders with no layout wrapper (a bare banner after
|
|
532
|
+
// login instead of the app). emailPasswordClient() brings the login
|
|
533
|
+
// screen + session provider — without it /login stays empty.
|
|
534
|
+
//
|
|
535
|
+
// Add new client plugins (e.g. notificationsClient()) to clientFeatures
|
|
536
|
+
// here — symmetric to APP_FEATURES on the server side.
|
|
537
|
+
|
|
538
|
+
${imports}
|
|
539
|
+
|
|
540
|
+
function AppShell({ children, schema }: { children: ReactNode; schema: AppSchema }): ReactNode {
|
|
541
|
+
return (
|
|
542
|
+
${renderDefaultAppShellElement(appName)}
|
|
543
|
+
{children}
|
|
544
|
+
</DefaultAppShell>
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
createKumikoApp({
|
|
549
|
+
shell: AppShell,
|
|
550
|
+
clientFeatures: [emailPasswordClient(), tasksClient],
|
|
551
|
+
});
|
|
552
|
+
`;
|
|
587
553
|
}
|
|
588
554
|
function renderEnvExample(appName) {
|
|
589
555
|
const devDb = `${appName.replace(/-/g, "_")}_dev`;
|
|
@@ -602,6 +568,11 @@ JWT_SECRET=change-me-min-32-chars-change-me-min-32
|
|
|
602
568
|
# Generate with: openssl rand -base64 32
|
|
603
569
|
KUMIKO_SECRETS_MASTER_KEY_V1=
|
|
604
570
|
|
|
571
|
+
# DEMO_ADMIN_PASSWORD: password of the admin seeded on first prod boot
|
|
572
|
+
# (admin@${appName}.local). Required by \`bun run boot\` and \`bun run start\`,
|
|
573
|
+
# min 12 chars. Generate with: openssl rand -hex 24
|
|
574
|
+
DEMO_ADMIN_PASSWORD=
|
|
575
|
+
|
|
605
576
|
# Subject-keys KMS for PII fields (user, tenant-invitation, fileRef). All-or-none.
|
|
606
577
|
# Unset → local boot with plaintext PII + warning; NODE_ENV=production requires
|
|
607
578
|
# the trio (requireKmsWiring). Generate PLATFORM_KEK / KUMIKO_BLIND_INDEX_KEY
|
|
@@ -636,6 +607,7 @@ services:
|
|
|
636
607
|
environment:
|
|
637
608
|
POSTGRES_USER: postgres
|
|
638
609
|
POSTGRES_PASSWORD: postgres
|
|
610
|
+
POSTGRES_DB: app
|
|
639
611
|
ports:
|
|
640
612
|
- "127.0.0.1:5432:5432"
|
|
641
613
|
volumes:
|
|
@@ -686,7 +658,8 @@ bun run boot
|
|
|
686
658
|
\`\`\`
|
|
687
659
|
|
|
688
660
|
Runs \`KUMIKO_DRY_RUN_ENV=boot bun bin/main.ts\` — validates feature composition
|
|
689
|
-
+ env schema, exits 0 without touching DB/Redis. Useful in CI.
|
|
661
|
+
+ env schema, exits 0 without touching DB/Redis. Useful in CI. It needs
|
|
662
|
+
\`DEMO_ADMIN_PASSWORD\` (min 12 chars, \`openssl rand -hex 24\`) in \`.env\`.
|
|
690
663
|
|
|
691
664
|
Without \`PLATFORM_KEK\` / \`SUBJECT_KEYS_DATABASE_URL\` / \`KUMIKO_BLIND_INDEX_KEY\`
|
|
692
665
|
set (see \`.env.example\`), both \`bun run boot\` and \`bun run start\` boot with
|
|
@@ -701,6 +674,9 @@ bun run schema:apply # apply checked-in kumiko/migrations (needs DATABASE_URL)
|
|
|
701
674
|
bun run start # runProdApp against dist/
|
|
702
675
|
\`\`\`
|
|
703
676
|
|
|
677
|
+
The first prod boot seeds \`admin@${appName}.local\` with the password from
|
|
678
|
+
\`DEMO_ADMIN_PASSWORD\` (required, min 12 chars). Log in with that.
|
|
679
|
+
|
|
704
680
|
After adding entities/features, regenerate migrations:
|
|
705
681
|
|
|
706
682
|
\`\`\`sh
|
|
@@ -741,9 +717,9 @@ function renderKumikoSchema() {
|
|
|
741
717
|
"// Computes table-metas from the SAME composeFeatures(APP_FEATURES) the",
|
|
742
718
|
"// runtime sees (runProdApp/runDevApp) — migration and runtime cannot drift.",
|
|
743
719
|
"",
|
|
744
|
-
'import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";',
|
|
745
720
|
'import { collectTableMetas, type EntityTableMeta } from "@cosmicdrift/kumiko-framework/db";',
|
|
746
721
|
'import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";',
|
|
722
|
+
'import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";',
|
|
747
723
|
'import { APP_FEATURES, HAS_AUTH } from "../src/run-config";',
|
|
748
724
|
"",
|
|
749
725
|
"export const FEATURES: readonly FeatureDefinition[] = composeFeatures([...APP_FEATURES], {",
|
|
@@ -774,10 +750,10 @@ function renderBinKumiko() {
|
|
|
774
750
|
"// ad-hoc SQL. kumiko-build bundles this file to dist-server/kumiko.js.",
|
|
775
751
|
"",
|
|
776
752
|
'import { frameworkCoreEnvSchema } from "@cosmicdrift/kumiko-dev-server/env-schema";',
|
|
777
|
-
'import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";',
|
|
778
753
|
'import { runConsumerCli } from "@cosmicdrift/kumiko-framework/consumer-cli";',
|
|
779
754
|
'import { composeEnvSchema, kmsSlotsOf } from "@cosmicdrift/kumiko-framework/env";',
|
|
780
755
|
'import { runSchemaCli } from "@cosmicdrift/kumiko-framework/schema-cli";',
|
|
756
|
+
'import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";',
|
|
781
757
|
'import { APP_FEATURES, HAS_AUTH } from "../src/run-config";',
|
|
782
758
|
"",
|
|
783
759
|
"const [, , cmd, ...rest] = Bun.argv;",
|
|
@@ -787,7 +763,9 @@ function renderBinKumiko() {
|
|
|
787
763
|
'if (cmd === "schema") {',
|
|
788
764
|
" const features = composeFeatures([...APP_FEATURES], { includeBundled: HAS_AUTH });",
|
|
789
765
|
" const kmsSlots = kmsSlotsOf(composeEnvSchema({ core: frameworkCoreEnvSchema, features }).schema);",
|
|
790
|
-
" process.exit(
|
|
766
|
+
" process.exit(",
|
|
767
|
+
" await runSchemaCli(rest, process.env.INIT_CWD ?? process.cwd(), out, { features, kmsSlots }),",
|
|
768
|
+
" );",
|
|
791
769
|
"}",
|
|
792
770
|
"",
|
|
793
771
|
'if (cmd === "consumer") {',
|
|
@@ -795,7 +773,9 @@ function renderBinKumiko() {
|
|
|
795
773
|
"}",
|
|
796
774
|
"",
|
|
797
775
|
"// biome-ignore lint/suspicious/noConsole: CLI output is the feature.",
|
|
798
|
-
|
|
776
|
+
"console.error(",
|
|
777
|
+
` \`\\n Unknown: kumiko \${cmd ?? ""} — only 'kumiko schema <sub>' or 'kumiko consumer <sub>' in the standalone bundle.\\n\`,`,
|
|
778
|
+
");",
|
|
799
779
|
"process.exit(1);",
|
|
800
780
|
"",
|
|
801
781
|
].join("\n");
|