@cosmicdrift/kumiko-dev-server 0.105.1 → 0.108.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/package.json +3 -3
- package/src/__tests__/compose-features-wiring.integration.test.ts +9 -9
- package/src/__tests__/compose-features.test.ts +6 -1
- package/src/__tests__/config-resolver-default.integration.test.ts +35 -1
- package/src/__tests__/require-env.test.ts +29 -0
- package/src/__tests__/run-dev-app.integration.test.ts +75 -1
- package/src/__tests__/scaffold-app-feature.test.ts +2 -2
- package/src/__tests__/scaffold-app.test.ts +137 -81
- package/src/__tests__/session-wiring.test.ts +14 -0
- package/src/__tests__/walkthrough.integration.test.ts +13 -19
- package/src/run-dev-app.ts +34 -4
- package/src/run-prod-app.ts +33 -14
- package/src/scaffold-app.ts +297 -20
package/src/scaffold-app.ts
CHANGED
|
@@ -12,9 +12,17 @@
|
|
|
12
12
|
// Static files (package.json, tsconfig, .env, README) stay text-based.
|
|
13
13
|
|
|
14
14
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
15
|
-
import { join, resolve } from "node:path";
|
|
15
|
+
import { join, relative, resolve } from "node:path";
|
|
16
|
+
import {
|
|
17
|
+
collectTableMetas,
|
|
18
|
+
generateMigration,
|
|
19
|
+
writeSnapshotJson,
|
|
20
|
+
} from "@cosmicdrift/kumiko-framework/db";
|
|
21
|
+
import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
16
22
|
import { IndentationText, Project, VariableDeclarationKind } from "ts-morph";
|
|
23
|
+
import { composeFeatures } from "./compose-features";
|
|
17
24
|
import { isKebabSegment } from "./kebab";
|
|
25
|
+
import { scaffoldDeploy } from "./scaffold-deploy";
|
|
18
26
|
|
|
19
27
|
// Single bundled-feature entry the scaffolder mounts into run-config.ts.
|
|
20
28
|
// importPath is the from-spec ("@cosmicdrift/kumiko-bundled-features/files"),
|
|
@@ -52,7 +60,7 @@ export type ScaffoldAppResult = {
|
|
|
52
60
|
readonly appName: string;
|
|
53
61
|
};
|
|
54
62
|
|
|
55
|
-
export function scaffoldApp(options: ScaffoldAppOptions): ScaffoldAppResult {
|
|
63
|
+
export async function scaffoldApp(options: ScaffoldAppOptions): Promise<ScaffoldAppResult> {
|
|
56
64
|
if (!isKebabSegment(options.name)) {
|
|
57
65
|
throw new Error(`scaffoldApp: name must be kebab-case (a-z, 0-9, -); got "${options.name}"`);
|
|
58
66
|
}
|
|
@@ -65,6 +73,7 @@ export function scaffoldApp(options: ScaffoldAppOptions): ScaffoldAppResult {
|
|
|
65
73
|
|
|
66
74
|
mkdirSync(join(destination, "bin"), { recursive: true });
|
|
67
75
|
mkdirSync(join(destination, "src"), { recursive: true });
|
|
76
|
+
mkdirSync(join(destination, "kumiko"), { recursive: true });
|
|
68
77
|
|
|
69
78
|
const files: string[] = [];
|
|
70
79
|
|
|
@@ -74,24 +83,52 @@ export function scaffoldApp(options: ScaffoldAppOptions): ScaffoldAppResult {
|
|
|
74
83
|
write(join(destination, "tsconfig.json"), renderTsconfig());
|
|
75
84
|
files.push("tsconfig.json");
|
|
76
85
|
|
|
86
|
+
write(join(destination, "biome.json"), renderBiomeJson());
|
|
87
|
+
files.push("biome.json");
|
|
88
|
+
|
|
89
|
+
write(join(destination, "bunfig.toml"), renderBunfigToml());
|
|
90
|
+
files.push("bunfig.toml");
|
|
91
|
+
|
|
92
|
+
write(join(destination, "bunfig.ci.toml"), renderBunfigCiToml());
|
|
93
|
+
files.push("bunfig.ci.toml");
|
|
94
|
+
|
|
77
95
|
write(join(destination, "src", "run-config.ts"), renderRunConfig(options.features));
|
|
78
96
|
files.push("src/run-config.ts");
|
|
79
97
|
|
|
98
|
+
write(join(destination, "kumiko", "schema.ts"), renderKumikoSchema());
|
|
99
|
+
files.push("kumiko/schema.ts");
|
|
100
|
+
|
|
80
101
|
write(join(destination, "bin", "main.ts"), renderMain(options.name));
|
|
81
102
|
files.push("bin/main.ts");
|
|
82
103
|
|
|
83
104
|
write(join(destination, "bin", "dev.ts"), renderDev(options.name));
|
|
84
105
|
files.push("bin/dev.ts");
|
|
85
106
|
|
|
107
|
+
write(join(destination, "bin", "kumiko.ts"), renderBinKumiko());
|
|
108
|
+
files.push("bin/kumiko.ts");
|
|
109
|
+
|
|
86
110
|
write(join(destination, "src", "client.tsx"), renderClient());
|
|
87
111
|
files.push("src/client.tsx");
|
|
88
112
|
|
|
113
|
+
write(join(destination, "src", "styles.css"), renderStylesCss());
|
|
114
|
+
files.push("src/styles.css");
|
|
115
|
+
|
|
89
116
|
write(join(destination, ".env.example"), renderEnvExample(options.name));
|
|
90
117
|
files.push(".env.example");
|
|
91
118
|
|
|
92
119
|
write(join(destination, "docker-compose.yml"), renderDockerCompose());
|
|
93
120
|
files.push("docker-compose.yml");
|
|
94
121
|
|
|
122
|
+
await writeInitMigration(destination, options.features);
|
|
123
|
+
files.push("kumiko/migrations/0001_init.sql", "kumiko/migrations/.snapshot.json");
|
|
124
|
+
|
|
125
|
+
const deploy = scaffoldDeploy({ appName: options.name, destination });
|
|
126
|
+
for (const f of deploy.files) {
|
|
127
|
+
if (f.written) {
|
|
128
|
+
files.push(relative(destination, f.path));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
95
132
|
write(join(destination, "README.md"), renderReadme(options.name, options.features));
|
|
96
133
|
files.push("README.md");
|
|
97
134
|
|
|
@@ -112,16 +149,31 @@ function renderPackageJson(name: string, version: string): string {
|
|
|
112
149
|
type: "module",
|
|
113
150
|
scripts: {
|
|
114
151
|
dev: "bun --watch bin/dev.ts",
|
|
152
|
+
build: "bun kumiko-build",
|
|
153
|
+
start: "bun run bin/main.ts",
|
|
115
154
|
boot: "KUMIKO_DRY_RUN_ENV=boot bun bin/main.ts",
|
|
116
|
-
|
|
155
|
+
typecheck: "tsc --noEmit",
|
|
156
|
+
lint: "biome check .",
|
|
157
|
+
test: "bun --config=bunfig.ci.toml test --dots",
|
|
158
|
+
"schema:apply": "bun kumiko-schema apply",
|
|
159
|
+
"schema:generate": "bun kumiko-schema generate",
|
|
117
160
|
},
|
|
118
161
|
dependencies: {
|
|
119
162
|
"@cosmicdrift/kumiko-bundled-features": version,
|
|
120
163
|
"@cosmicdrift/kumiko-dev-server": version,
|
|
121
164
|
"@cosmicdrift/kumiko-framework": version,
|
|
122
165
|
"@cosmicdrift/kumiko-renderer-web": version,
|
|
166
|
+
react: "^19.2.6",
|
|
167
|
+
"react-dom": "^19.2.6",
|
|
123
168
|
zod: "^4.4.3",
|
|
124
169
|
},
|
|
170
|
+
devDependencies: {
|
|
171
|
+
"@biomejs/biome": "^2.4.15",
|
|
172
|
+
"@tailwindcss/cli": "^4.3.0",
|
|
173
|
+
"bun-types": "^1.3.14",
|
|
174
|
+
tailwindcss: "^4.3.0",
|
|
175
|
+
typescript: "^6.0.3",
|
|
176
|
+
},
|
|
125
177
|
},
|
|
126
178
|
null,
|
|
127
179
|
2,
|
|
@@ -141,17 +193,107 @@ function renderTsconfig(): string {
|
|
|
141
193
|
moduleResolution: "bundler",
|
|
142
194
|
esModuleInterop: true,
|
|
143
195
|
skipLibCheck: true,
|
|
144
|
-
lib: ["ESNext"],
|
|
196
|
+
lib: ["ESNext", "DOM"],
|
|
145
197
|
types: ["bun-types"],
|
|
198
|
+
jsx: "react-jsx",
|
|
146
199
|
noEmit: true,
|
|
147
200
|
},
|
|
148
|
-
include: ["bin", "src"],
|
|
201
|
+
include: ["bin", "src", "kumiko"],
|
|
149
202
|
},
|
|
150
203
|
null,
|
|
151
204
|
2,
|
|
152
205
|
)}\n`;
|
|
153
206
|
}
|
|
154
207
|
|
|
208
|
+
function renderBiomeJson(): string {
|
|
209
|
+
return `${JSON.stringify(
|
|
210
|
+
{
|
|
211
|
+
$schema: "https://biomejs.dev/schemas/2.4.15/schema.json",
|
|
212
|
+
vcs: {
|
|
213
|
+
enabled: true,
|
|
214
|
+
clientKind: "git",
|
|
215
|
+
useIgnoreFile: true,
|
|
216
|
+
defaultBranch: "main",
|
|
217
|
+
},
|
|
218
|
+
files: {
|
|
219
|
+
includes: ["src/**", "bin/**", "kumiko/**", "!**/dist", "!kumiko/migrations"],
|
|
220
|
+
},
|
|
221
|
+
formatter: {
|
|
222
|
+
enabled: true,
|
|
223
|
+
indentStyle: "space",
|
|
224
|
+
indentWidth: 2,
|
|
225
|
+
lineWidth: 100,
|
|
226
|
+
lineEnding: "lf",
|
|
227
|
+
},
|
|
228
|
+
css: {
|
|
229
|
+
parser: { cssModules: false, tailwindDirectives: true },
|
|
230
|
+
},
|
|
231
|
+
javascript: {
|
|
232
|
+
formatter: {
|
|
233
|
+
quoteStyle: "double",
|
|
234
|
+
jsxQuoteStyle: "double",
|
|
235
|
+
semicolons: "always",
|
|
236
|
+
trailingCommas: "all",
|
|
237
|
+
arrowParentheses: "always",
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
json: { formatter: { indentWidth: 2, lineWidth: 80 } },
|
|
241
|
+
linter: {
|
|
242
|
+
enabled: true,
|
|
243
|
+
rules: {
|
|
244
|
+
recommended: true,
|
|
245
|
+
correctness: { noUnusedVariables: "error", noUnusedImports: "error" },
|
|
246
|
+
suspicious: { noExplicitAny: "error", noDebugger: "error", noConsole: "warn" },
|
|
247
|
+
complexity: { useLiteralKeys: "off" },
|
|
248
|
+
style: { useConst: "error" },
|
|
249
|
+
nursery: { noFloatingPromises: "error" },
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
overrides: [
|
|
253
|
+
{
|
|
254
|
+
includes: ["**/*.test.ts", "**/*.spec.ts", "**/*.integration.ts", "**/*.e2e.ts"],
|
|
255
|
+
linter: {
|
|
256
|
+
rules: {
|
|
257
|
+
suspicious: { noConsole: "off" },
|
|
258
|
+
style: { noNonNullAssertion: "off" },
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
},
|
|
264
|
+
null,
|
|
265
|
+
2,
|
|
266
|
+
)}\n`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function renderBunfigToml(): string {
|
|
270
|
+
return `[install]
|
|
271
|
+
linker = "hoisted"
|
|
272
|
+
|
|
273
|
+
[test]
|
|
274
|
+
concurrency = 8
|
|
275
|
+
pathIgnorePatterns = [
|
|
276
|
+
"**/e2e/**",
|
|
277
|
+
"**/*.spec.ts",
|
|
278
|
+
"**/dist/**",
|
|
279
|
+
]
|
|
280
|
+
`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function renderBunfigCiToml(): string {
|
|
284
|
+
return `[install]
|
|
285
|
+
linker = "hoisted"
|
|
286
|
+
|
|
287
|
+
[test]
|
|
288
|
+
concurrency = 8
|
|
289
|
+
pathIgnorePatterns = [
|
|
290
|
+
"**/e2e/**",
|
|
291
|
+
"**/*.spec.ts",
|
|
292
|
+
"**/dist/**",
|
|
293
|
+
]
|
|
294
|
+
`;
|
|
295
|
+
}
|
|
296
|
+
|
|
155
297
|
function newTsProject(): Project {
|
|
156
298
|
return new Project({
|
|
157
299
|
useInMemoryFileSystem: true,
|
|
@@ -187,8 +329,13 @@ function renderRunConfig(features?: ReadonlyArray<ScaffoldFeatureEntry>): string
|
|
|
187
329
|
const project = newTsProject();
|
|
188
330
|
const sf = project.createSourceFile("run-config.ts", "");
|
|
189
331
|
|
|
190
|
-
|
|
191
|
-
|
|
332
|
+
// Fallback decided BEFORE filtering: if the caller passed features (even
|
|
333
|
+
// if every one of them is an auto-mounted name), an all-filtered-out empty
|
|
334
|
+
// result is the correct outcome — falling back to FOUNDATION_FEATURES here
|
|
335
|
+
// would silently substitute a different feature set than the caller asked
|
|
336
|
+
// for.
|
|
337
|
+
const base = features?.length ? features : FOUNDATION_FEATURES;
|
|
338
|
+
const effective = base.filter((f) => !COMPOSE_AUTO_MOUNTED_NAMES.has(f.name));
|
|
192
339
|
const grouped = new Map<string, string[]>();
|
|
193
340
|
for (const entry of effective) {
|
|
194
341
|
const existing = grouped.get(entry.importPath) ?? [];
|
|
@@ -211,6 +358,12 @@ function renderRunConfig(features?: ReadonlyArray<ScaffoldFeatureEntry>): string
|
|
|
211
358
|
],
|
|
212
359
|
});
|
|
213
360
|
|
|
361
|
+
sf.addVariableStatement({
|
|
362
|
+
declarationKind: VariableDeclarationKind.Const,
|
|
363
|
+
isExported: true,
|
|
364
|
+
declarations: [{ name: "HAS_AUTH", initializer: "true" }],
|
|
365
|
+
});
|
|
366
|
+
|
|
214
367
|
sf.insertText(
|
|
215
368
|
0,
|
|
216
369
|
[
|
|
@@ -250,7 +403,7 @@ function renderMain(appName: string): string {
|
|
|
250
403
|
});
|
|
251
404
|
sf.addImportDeclaration({
|
|
252
405
|
moduleSpecifier: "../src/run-config",
|
|
253
|
-
namedImports: ["APP_FEATURES"],
|
|
406
|
+
namedImports: ["APP_FEATURES", "HAS_AUTH"],
|
|
254
407
|
});
|
|
255
408
|
|
|
256
409
|
sf.addVariableStatement({
|
|
@@ -265,7 +418,7 @@ function renderMain(appName: string): string {
|
|
|
265
418
|
|
|
266
419
|
// The envSchema must cover the SAME features runProdApp mounts at boot.
|
|
267
420
|
// `auth: { admin: … }` below makes runProdApp auto-mix config/user/tenant/
|
|
268
|
-
// auth-email-password via composeFeatures(includeBundled:
|
|
421
|
+
// auth-email-password via composeFeatures(includeBundled:HAS_AUTH); compose the
|
|
269
422
|
// identical set here so the auth feature's JWT_SECRET (min-32) declaration
|
|
270
423
|
// is part of the boot-gate — otherwise a too-short JWT_SECRET slips through.
|
|
271
424
|
sf.addVariableStatement({
|
|
@@ -273,7 +426,7 @@ function renderMain(appName: string): string {
|
|
|
273
426
|
declarations: [
|
|
274
427
|
{
|
|
275
428
|
name: "bootFeatures",
|
|
276
|
-
initializer: "composeFeatures(APP_FEATURES, { includeBundled:
|
|
429
|
+
initializer: "composeFeatures(APP_FEATURES, { includeBundled: HAS_AUTH })",
|
|
277
430
|
},
|
|
278
431
|
],
|
|
279
432
|
});
|
|
@@ -294,7 +447,7 @@ function renderMain(appName: string): string {
|
|
|
294
447
|
.inlineBlock(() => {
|
|
295
448
|
writer.writeLine("features: APP_FEATURES,");
|
|
296
449
|
writer.writeLine("envSchema,");
|
|
297
|
-
writer.writeLine(
|
|
450
|
+
writer.writeLine('staticDir: "./dist",');
|
|
298
451
|
writer.write("auth: ").inlineBlock(() => {
|
|
299
452
|
writer.write("admin: ").inlineBlock(() => {
|
|
300
453
|
writer.writeLine(`email: "admin@${appName}.local",`);
|
|
@@ -458,19 +611,20 @@ KUMIKO_DEV_DB_NAME=${devDb}
|
|
|
458
611
|
`;
|
|
459
612
|
}
|
|
460
613
|
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
// Redis must not be reachable from the LAN on a machine without a firewall.
|
|
614
|
+
// Ports + credentials match the *_URL defaults in renderEnvExample, so
|
|
615
|
+
// `docker compose up -d` just works with the generated .env. Named pg volume
|
|
616
|
+
// so dev data survives `docker compose down` (pairs with KUMIKO_DEV_DB_NAME
|
|
617
|
+
// persistence) — the loopback-binding rationale is in the generated file's
|
|
618
|
+
// own comment (657/1), no need to duplicate it here.
|
|
467
619
|
function renderDockerCompose(): string {
|
|
468
620
|
return `# Local Postgres + Redis for \`bun dev\`. Matches the *_URL defaults in .env.example.
|
|
469
621
|
# Start: docker compose up -d · Stop: docker compose down · Reset: docker compose down -v
|
|
470
622
|
# Ports bind to 127.0.0.1 only — weak dev credentials must not be exposed on the LAN.
|
|
471
623
|
services:
|
|
472
624
|
postgres:
|
|
473
|
-
|
|
625
|
+
# Pinned to the project's own compose-file tag (663/1) — Alpine variant
|
|
626
|
+
# (~90MB vs ~400MB) and a reproducible patch version, bump on PG18 minors.
|
|
627
|
+
image: postgres:18.3-alpine
|
|
474
628
|
environment:
|
|
475
629
|
POSTGRES_USER: postgres
|
|
476
630
|
POSTGRES_PASSWORD: postgres
|
|
@@ -530,17 +684,140 @@ bun run boot
|
|
|
530
684
|
Runs \`KUMIKO_DRY_RUN_ENV=boot bun bin/main.ts\` — validates feature composition
|
|
531
685
|
+ env schema, exits 0 without touching DB/Redis. Useful in CI.
|
|
532
686
|
|
|
687
|
+
## Production build + schema
|
|
688
|
+
|
|
689
|
+
\`\`\`sh
|
|
690
|
+
bun run build # kumiko-build → dist/ + dist-server/
|
|
691
|
+
bun run schema:apply # apply checked-in kumiko/migrations (needs DATABASE_URL)
|
|
692
|
+
bun run start # runProdApp against dist/
|
|
693
|
+
\`\`\`
|
|
694
|
+
|
|
695
|
+
After adding entities/features, regenerate migrations:
|
|
696
|
+
|
|
697
|
+
\`\`\`sh
|
|
698
|
+
bun run schema:generate <name>
|
|
699
|
+
\`\`\`
|
|
700
|
+
|
|
701
|
+
## Deploy
|
|
702
|
+
|
|
703
|
+
\`deploy/Dockerfile\` + \`deploy/migrate-step.sh\` are scaffolded for container
|
|
704
|
+
deploys. Build context = app repo root; migrations ship in \`kumiko/migrations/\`.
|
|
705
|
+
|
|
533
706
|
## Architecture
|
|
534
707
|
|
|
535
|
-
- \`src/run-config.ts\` — single source of truth: which features your app mounts.
|
|
708
|
+
- \`src/run-config.ts\` — single source of truth: which features your app mounts (\`APP_FEATURES\`, \`HAS_AUTH\`).
|
|
709
|
+
- \`kumiko/schema.ts\` — same feature set → \`ENTITY_METAS\` for \`kumiko schema\`.
|
|
536
710
|
- \`bin/dev.ts\` — dev-server entry (\`bun dev\`).
|
|
537
|
-
- \`bin/main.ts\` — production-bootstrap (\`bun run
|
|
711
|
+
- \`bin/main.ts\` — production-bootstrap (\`bun run start\`).
|
|
712
|
+
- \`bin/kumiko.ts\` — schema-CLI bundled into \`dist-server/kumiko.js\`.
|
|
538
713
|
- \`docker-compose.yml\` — local Postgres + Redis for \`bun dev\`.
|
|
539
714
|
|
|
540
715
|
For full docs see https://docs.kumiko.rocks.
|
|
541
716
|
`;
|
|
542
717
|
}
|
|
543
718
|
|
|
719
|
+
function renderStylesCss(): string {
|
|
720
|
+
return [
|
|
721
|
+
'@import "@cosmicdrift/kumiko-renderer-web/styles.css";',
|
|
722
|
+
"",
|
|
723
|
+
'@source "./**/*.{ts,tsx}";',
|
|
724
|
+
"",
|
|
725
|
+
].join("\n");
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function renderKumikoSchema(): string {
|
|
729
|
+
return [
|
|
730
|
+
"// Live ENTITY_METAS source for `kumiko schema generate|apply|status`.",
|
|
731
|
+
"//",
|
|
732
|
+
"// Computes table-metas from the SAME composeFeatures(APP_FEATURES) the",
|
|
733
|
+
"// runtime sees (runProdApp/runDevApp) — migration and runtime cannot drift.",
|
|
734
|
+
"",
|
|
735
|
+
'import { composeFeatures } from "@cosmicdrift/kumiko-dev-server/compose-features";',
|
|
736
|
+
'import { collectTableMetas, type EntityTableMeta } from "@cosmicdrift/kumiko-framework/db";',
|
|
737
|
+
'import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";',
|
|
738
|
+
'import { APP_FEATURES, HAS_AUTH } from "../src/run-config";',
|
|
739
|
+
"",
|
|
740
|
+
"export const FEATURES: readonly FeatureDefinition[] = composeFeatures([...APP_FEATURES], {",
|
|
741
|
+
" includeBundled: HAS_AUTH,",
|
|
742
|
+
"});",
|
|
743
|
+
"",
|
|
744
|
+
"export const ENTITY_METAS: readonly EntityTableMeta[] = collectTableMetas(FEATURES);",
|
|
745
|
+
"",
|
|
746
|
+
].join("\n");
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function renderBinKumiko(): string {
|
|
750
|
+
return [
|
|
751
|
+
"#!/usr/bin/env bun",
|
|
752
|
+
"",
|
|
753
|
+
"// Standalone kumiko schema-CLI for the production bundle. The deploy",
|
|
754
|
+
"// migrate-step runs `bun /app/kumiko.js schema apply`; kumiko-build bundles",
|
|
755
|
+
"// this file to dist-server/kumiko.js.",
|
|
756
|
+
"",
|
|
757
|
+
'import { composeFeatures } from "@cosmicdrift/kumiko-dev-server/compose-features";',
|
|
758
|
+
'import { runSchemaCli } from "@cosmicdrift/kumiko-framework/schema-cli";',
|
|
759
|
+
'import { APP_FEATURES, HAS_AUTH } from "../src/run-config";',
|
|
760
|
+
"",
|
|
761
|
+
"const [, , cmd, ...rest] = Bun.argv;",
|
|
762
|
+
'if (cmd !== "schema") {',
|
|
763
|
+
" // biome-ignore lint/suspicious/noConsole: CLI output is the feature.",
|
|
764
|
+
' console.error("\\n Unbekannt: kumiko " + (cmd ?? "") + " — nur \'kumiko schema <sub>\' im Standalone-Bundle.\\n");',
|
|
765
|
+
" process.exit(1);",
|
|
766
|
+
"}",
|
|
767
|
+
"",
|
|
768
|
+
"const features = composeFeatures([...APP_FEATURES], { includeBundled: HAS_AUTH });",
|
|
769
|
+
"// biome-ignore lint/suspicious/noConsole: CLI output is the feature.",
|
|
770
|
+
"const out = { log: (l: string) => console.log(l), err: (l: string) => console.error(l) };",
|
|
771
|
+
"process.exit(await runSchemaCli(rest, process.env.INIT_CWD ?? process.cwd(), out, { features }));",
|
|
772
|
+
"",
|
|
773
|
+
].join("\n");
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
async function instantiateScaffoldFeatures(
|
|
777
|
+
features?: ReadonlyArray<ScaffoldFeatureEntry>,
|
|
778
|
+
): Promise<readonly FeatureDefinition[]> {
|
|
779
|
+
const base = features?.length ? features : FOUNDATION_FEATURES;
|
|
780
|
+
const effective = base.filter((f) => !COMPOSE_AUTO_MOUNTED_NAMES.has(f.name));
|
|
781
|
+
const instances: FeatureDefinition[] = [];
|
|
782
|
+
for (const entry of effective) {
|
|
783
|
+
const mod = (await import(entry.importPath)) as Record<string, unknown>;
|
|
784
|
+
const exp = mod[entry.exportName];
|
|
785
|
+
if (exp === undefined) {
|
|
786
|
+
throw new Error(
|
|
787
|
+
`scaffoldApp: ${entry.importPath} missing export ${entry.exportName} for ${entry.callExpression}`,
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
if (entry.callExpression.endsWith("()")) {
|
|
791
|
+
if (typeof exp !== "function") {
|
|
792
|
+
throw new Error(`scaffoldApp: ${entry.exportName} is not callable (${entry.importPath})`);
|
|
793
|
+
}
|
|
794
|
+
instances.push((exp as () => FeatureDefinition)());
|
|
795
|
+
} else {
|
|
796
|
+
instances.push(exp as FeatureDefinition);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return instances;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
async function writeInitMigration(
|
|
803
|
+
destination: string,
|
|
804
|
+
features?: ReadonlyArray<ScaffoldFeatureEntry>,
|
|
805
|
+
): Promise<void> {
|
|
806
|
+
const instances = await instantiateScaffoldFeatures(features);
|
|
807
|
+
const composed = composeFeatures(instances, { includeBundled: true });
|
|
808
|
+
const metas = collectTableMetas(composed);
|
|
809
|
+
const result = generateMigration({
|
|
810
|
+
metas,
|
|
811
|
+
prevSnapshot: null,
|
|
812
|
+
name: "init",
|
|
813
|
+
sequenceNumber: 1,
|
|
814
|
+
});
|
|
815
|
+
const migrationsDir = join(destination, "kumiko", "migrations");
|
|
816
|
+
mkdirSync(migrationsDir, { recursive: true });
|
|
817
|
+
writeFileSync(join(migrationsDir, result.filename), result.sqlContent);
|
|
818
|
+
writeSnapshotJson(join(migrationsDir, ".snapshot.json"), result.snapshot);
|
|
819
|
+
}
|
|
820
|
+
|
|
544
821
|
// Deterministic tenant-ID from app-name. Format: UUID-v4 with the
|
|
545
822
|
// version-marker at the right spot. NOT cryptographically random —
|
|
546
823
|
// just a stable per-app default the user can change later.
|