@cosmicdrift/kumiko-dev-server 0.329.0 → 0.330.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.
Files changed (125) hide show
  1. package/README.md +17 -0
  2. package/bin/kumiko-build.ts +3 -2
  3. package/bin/kumiko-dev.ts +1 -1
  4. package/bin/kumiko-init-deploy.ts +1 -1
  5. package/bin/kumiko-schema-check.ts +4 -1
  6. package/dist/build-server-bundle.d.ts +31 -0
  7. package/dist/build-server-bundle.js +282 -0
  8. package/dist/build.d.ts +2 -0
  9. package/dist/build.js +5 -0
  10. package/dist/cli.d.ts +5 -0
  11. package/dist/cli.js +9 -0
  12. package/dist/codegen/index.d.ts +4 -0
  13. package/dist/codegen/index.js +4 -0
  14. package/dist/codegen/render.d.ts +55 -0
  15. package/dist/codegen/render.js +273 -0
  16. package/dist/codegen/run-codegen.d.ts +20 -0
  17. package/dist/codegen/run-codegen.js +219 -0
  18. package/dist/codegen/scan-events.d.ts +76 -0
  19. package/dist/codegen/scan-events.js +447 -0
  20. package/dist/codegen/watch.d.ts +33 -0
  21. package/dist/codegen/watch.js +112 -0
  22. package/dist/compose-stacks.d.ts +60 -0
  23. package/dist/compose-stacks.js +127 -0
  24. package/dist/crash-tracker.d.ts +17 -0
  25. package/dist/crash-tracker.js +37 -0
  26. package/dist/create-kumiko-server.d.ts +195 -0
  27. package/dist/create-kumiko-server.js +1040 -0
  28. package/dist/env-schema.d.ts +15 -0
  29. package/dist/env-schema.js +60 -0
  30. package/dist/few-shot-corpus.d.ts +72 -0
  31. package/dist/few-shot-corpus.js +262 -0
  32. package/dist/framework-version.d.ts +1 -0
  33. package/dist/framework-version.js +31 -0
  34. package/dist/index.d.ts +22 -0
  35. package/dist/index.js +33 -0
  36. package/dist/init-deploy-cli.d.ts +10 -0
  37. package/dist/init-deploy-cli.js +111 -0
  38. package/dist/kebab.d.ts +1 -0
  39. package/{src/kebab.ts → dist/kebab.js} +2 -3
  40. package/dist/run-dev-app.d.ts +174 -0
  41. package/dist/run-dev-app.js +403 -0
  42. package/dist/scaffold-app-feature.d.ts +16 -0
  43. package/dist/scaffold-app-feature.js +240 -0
  44. package/dist/scaffold-app.d.ts +45 -0
  45. package/dist/scaffold-app.js +852 -0
  46. package/dist/scaffold-demo-tasks.d.ts +8 -0
  47. package/{src/scaffold-demo-tasks.ts → dist/scaffold-demo-tasks.js} +69 -98
  48. package/dist/scaffold-deploy.d.ts +92 -0
  49. package/dist/scaffold-deploy.js +254 -0
  50. package/dist/scaffold-feature.d.ts +26 -0
  51. package/dist/scaffold-feature.js +163 -0
  52. package/dist/schema-apply.d.ts +11 -0
  53. package/dist/schema-apply.js +95 -0
  54. package/dist/schema-check-core.d.ts +2 -0
  55. package/{src/schema-check-core.ts → dist/schema-check-core.js} +8 -10
  56. package/dist/schema-tables-auth-mode.d.ts +2 -0
  57. package/{src/schema-tables-auth-mode.ts → dist/schema-tables-auth-mode.js} +2 -10
  58. package/dist/schema-tables-minimal.d.ts +2 -0
  59. package/{src/schema-tables-minimal.ts → dist/schema-tables-minimal.js} +2 -11
  60. package/dist/setup-test-stack-from-features.d.ts +11 -0
  61. package/dist/setup-test-stack-from-features.js +42 -0
  62. package/dist/welcome-banner.d.ts +15 -0
  63. package/dist/welcome-banner.js +27 -0
  64. package/package.json +71 -24
  65. package/src/changes.json +6 -0
  66. package/src/__tests__/build-prod-bundle.integration.test.ts +0 -282
  67. package/src/__tests__/build-server-bundle.test.ts +0 -228
  68. package/src/__tests__/classify-change.test.ts +0 -87
  69. package/src/__tests__/compose-stacks.test.ts +0 -273
  70. package/src/__tests__/config-resolver-default.integration.test.ts +0 -165
  71. package/src/__tests__/crash-tracker.test.ts +0 -89
  72. package/src/__tests__/create-kumiko-server-errors.test.ts +0 -54
  73. package/src/__tests__/create-kumiko-server-options.test.ts +0 -33
  74. package/src/__tests__/create-kumiko-server.integration.test.ts +0 -1007
  75. package/src/__tests__/discover-format.test.ts +0 -32
  76. package/src/__tests__/env-schema.integration.test.ts +0 -248
  77. package/src/__tests__/env-schema.test.ts +0 -132
  78. package/src/__tests__/few-shot-corpus.test.ts +0 -316
  79. package/src/__tests__/framework-version.test.ts +0 -45
  80. package/src/__tests__/init-deploy-cli.test.ts +0 -113
  81. package/src/__tests__/kumiko-schema-check.test.ts +0 -56
  82. package/src/__tests__/merge-extra-context.test.ts +0 -67
  83. package/src/__tests__/resolve-stylesheet.test.ts +0 -132
  84. package/src/__tests__/run-dev-app-boot-validation.test.ts +0 -38
  85. package/src/__tests__/run-dev-app.integration.test.ts +0 -290
  86. package/src/__tests__/runtime-classification.test.ts +0 -26
  87. package/src/__tests__/saas-identity-wire.integration.test.ts +0 -427
  88. package/src/__tests__/scaffold-app-feature.test.ts +0 -216
  89. package/src/__tests__/scaffold-app.test.ts +0 -630
  90. package/src/__tests__/scaffold-demo-tasks.test.ts +0 -47
  91. package/src/__tests__/scaffold-deploy.test.ts +0 -464
  92. package/src/__tests__/scaffold-dev-cycle.integration.test.ts +0 -109
  93. package/src/__tests__/scaffold-feature.test.ts +0 -143
  94. package/src/__tests__/schema-apply.integration.test.ts +0 -191
  95. package/src/__tests__/setup-test-stack-from-features.integration.test.ts +0 -30
  96. package/src/__tests__/setup-test-stack-job-run-logger.integration.test.ts +0 -72
  97. package/src/__tests__/walkthrough.integration.test.ts +0 -140
  98. package/src/__tests__/welcome-banner.test.ts +0 -42
  99. package/src/build-server-bundle.ts +0 -346
  100. package/src/build.ts +0 -28
  101. package/src/codegen/__tests__/render-codegen.test.ts +0 -100
  102. package/src/codegen/__tests__/run-codegen.test.ts +0 -665
  103. package/src/codegen/__tests__/strict-mode-diagnostics.test.ts +0 -347
  104. package/src/codegen/__tests__/watch.test.ts +0 -222
  105. package/src/codegen/index.ts +0 -19
  106. package/src/codegen/render.ts +0 -305
  107. package/src/codegen/run-codegen.ts +0 -278
  108. package/src/codegen/scan-events.ts +0 -565
  109. package/src/codegen/watch.ts +0 -140
  110. package/src/compose-stacks.ts +0 -193
  111. package/src/crash-tracker.ts +0 -56
  112. package/src/create-kumiko-server.ts +0 -1397
  113. package/src/env-schema.ts +0 -72
  114. package/src/few-shot-corpus.ts +0 -370
  115. package/src/framework-version.ts +0 -39
  116. package/src/index.ts +0 -105
  117. package/src/init-deploy-cli.ts +0 -148
  118. package/src/run-dev-app.ts +0 -694
  119. package/src/scaffold-app-feature.ts +0 -283
  120. package/src/scaffold-app.ts +0 -1033
  121. package/src/scaffold-deploy.ts +0 -393
  122. package/src/scaffold-feature.ts +0 -226
  123. package/src/schema-apply.ts +0 -123
  124. package/src/setup-test-stack-from-features.ts +0 -80
  125. package/src/welcome-banner.ts +0 -43
@@ -1,1033 +0,0 @@
1
- // scaffoldApp — generate a runnable Kumiko app workspace from a name.
2
- //
3
- // Used by `kumiko new app <name>`. Produces the minimal app shape that
4
- // `KUMIKO_DRY_RUN_ENV=boot bun bin/main.ts` runs successfully against:
5
- // run-config with 5 foundation features, bin/main.ts with auth-admin
6
- // stub, package.json with @cosmicdrift/* deps, tsconfig, .env.example,
7
- // README.
8
- //
9
- // .ts files are built via ts-morph (same tool [[scaffoldAppFeature]] uses
10
- // to auto-mount features). Means a single AST representation for both
11
- // generate + later modify — no template-string ↔ ts-morph divergence.
12
- // Static files (package.json, tsconfig, .env, README) stay text-based.
13
-
14
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
15
- import { dirname, 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";
22
- import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";
23
- import { IndentationText, Project, VariableDeclarationKind } from "ts-morph";
24
- import { isKebabSegment } from "./kebab";
25
- import {
26
- createDemoTasksFeature,
27
- renderDemoSeedFile,
28
- renderDemoTasksFeatureFile,
29
- renderDemoTasksI18n,
30
- renderDemoTasksIndex,
31
- renderDemoTasksWebIndex,
32
- } from "./scaffold-demo-tasks";
33
- import { scaffoldDeploy } from "./scaffold-deploy";
34
-
35
- // Single bundled-feature entry the scaffolder mounts into run-config.ts.
36
- // importPath is the from-spec ("@cosmicdrift/kumiko-bundled-features/files"),
37
- // exportName the named import, callExpression the form that lands in the
38
- // APP_FEATURES array literal — typically `${exportName}()` for factory-style
39
- // features and just `${exportName}` for object-style ones (e.g. billingFoundationFeature).
40
- export type ScaffoldFeatureEntry = {
41
- readonly name: string;
42
- readonly importPath: string;
43
- readonly exportName: string;
44
- readonly callExpression: string;
45
- /** Runtime args for factory-style exports that need config the codegen text can't parse back out (e.g. `{ scopes: {} }`). Omit for zero-arg factories and object-style exports. */
46
- readonly callArgs?: readonly unknown[];
47
- };
48
-
49
- // Structural twin of `ScaffoldTestSetup` from @cosmicdrift/kumiko-testing/scaffold —
50
- // dev-server cannot import that package (it depends on dev-server), callers pass it in.
51
- export type ScaffoldTestSetup = {
52
- readonly files: Readonly<Record<string, string>>;
53
- readonly scripts: Readonly<Record<string, string>>;
54
- readonly devDependencies: Readonly<Record<string, string>>;
55
- readonly rulesMarkdown: string;
56
- };
57
-
58
- export type ScaffoldTestSetupFactory = (input: {
59
- readonly appName: string;
60
- readonly frameworkVersion: string;
61
- }) => ScaffoldTestSetup;
62
-
63
- export type ScaffoldAppOptions = {
64
- /** kebab-case app name (e.g. "my-shop"). Becomes package-name + folder. */
65
- readonly name: string;
66
- /** Absolute or cwd-relative target dir. Default: <cwd>/<name>. */
67
- readonly destination?: string;
68
- /** Base dir a relative `destination` (or the name-default) resolves
69
- * against. Defaults to process.cwd(). Callers with their own cwd-notion
70
- * (the CLI's ctx.cwd) MUST pass it so the scaffold lands where the
71
- * command's output claims it does. */
72
- readonly cwd?: string;
73
- /** npm-version-pin for @cosmicdrift/* deps. Default "*" for latest. */
74
- readonly frameworkVersion?: string;
75
- /** Bundled-features to mount in run-config.ts. Default: secrets + auth-foundation + sessions
76
- * (the historical foundation). create-kumiko-app passes the picker output
77
- * here so the generated APP_FEATURES reflects the user's selection. */
78
- readonly features?: ReadonlyArray<ScaffoldFeatureEntry>;
79
- /** Test wiring (bunfigs, scripts, example tests, e2e) — `renderTestSetup` from
80
- * @cosmicdrift/kumiko-testing/scaffold. Without it the app gets the legacy
81
- * bunfig pair and no example tests. */
82
- readonly testSetup?: ScaffoldTestSetupFactory;
83
- };
84
-
85
- export type ScaffoldAppResult = {
86
- readonly destination: string;
87
- readonly files: readonly string[];
88
- readonly appName: string;
89
- };
90
-
91
- export async function scaffoldApp(options: ScaffoldAppOptions): Promise<ScaffoldAppResult> {
92
- if (!isKebabSegment(options.name)) {
93
- throw new Error(`scaffoldApp: name must be kebab-case (a-z, 0-9, -); got "${options.name}"`);
94
- }
95
- const cwd = options.cwd ?? process.cwd();
96
- const destination = resolve(cwd, options.destination ?? options.name);
97
- if (existsSync(destination)) {
98
- throw new Error(`scaffoldApp: ${destination} already exists — refusing to overwrite`);
99
- }
100
- const version = options.frameworkVersion ?? "*";
101
- const testSetup = options.testSetup?.({ appName: options.name, frameworkVersion: version });
102
-
103
- mkdirSync(join(destination, "bin"), { recursive: true });
104
- mkdirSync(join(destination, "src"), { recursive: true });
105
- mkdirSync(join(destination, "kumiko"), { recursive: true });
106
-
107
- const files: string[] = [];
108
-
109
- write(join(destination, "package.json"), renderPackageJson(options.name, version, testSetup));
110
- files.push("package.json");
111
-
112
- write(join(destination, "tsconfig.json"), renderTsconfig());
113
- files.push("tsconfig.json");
114
-
115
- write(join(destination, "biome.json"), renderBiomeJson());
116
- files.push("biome.json");
117
-
118
- if (testSetup === undefined) {
119
- write(join(destination, "bunfig.toml"), renderBunfigToml());
120
- files.push("bunfig.toml");
121
-
122
- write(join(destination, "bunfig.ci.toml"), renderBunfigCiToml());
123
- files.push("bunfig.ci.toml");
124
- }
125
-
126
- write(join(destination, "src", "run-config.ts"), renderRunConfig(options.features));
127
- files.push("src/run-config.ts");
128
-
129
- mkdirSync(join(destination, "src", "features", "tasks", "web"), { recursive: true });
130
- write(join(destination, "src", "features", "tasks", "feature.ts"), renderDemoTasksFeatureFile());
131
- files.push("src/features/tasks/feature.ts");
132
- write(join(destination, "src", "features", "tasks", "i18n.ts"), renderDemoTasksI18n());
133
- files.push("src/features/tasks/i18n.ts");
134
- write(join(destination, "src", "features", "tasks", "index.ts"), renderDemoTasksIndex());
135
- files.push("src/features/tasks/index.ts");
136
- write(
137
- join(destination, "src", "features", "tasks", "web", "index.ts"),
138
- renderDemoTasksWebIndex(),
139
- );
140
- files.push("src/features/tasks/web/index.ts");
141
- write(join(destination, "src", "seed.ts"), renderDemoSeedFile());
142
- files.push("src/seed.ts");
143
-
144
- write(join(destination, "kumiko", "schema.ts"), renderKumikoSchema());
145
- files.push("kumiko/schema.ts");
146
-
147
- write(join(destination, "bin", "main.ts"), renderMain(options.name));
148
- files.push("bin/main.ts");
149
-
150
- write(join(destination, "bin", "dev.ts"), renderDev(options.name));
151
- files.push("bin/dev.ts");
152
-
153
- write(join(destination, "bin", "kumiko.ts"), renderBinKumiko());
154
- files.push("bin/kumiko.ts");
155
-
156
- write(join(destination, "src", "client.tsx"), renderClient(options.name));
157
- files.push("src/client.tsx");
158
-
159
- write(join(destination, "src", "styles.css"), renderStylesCss());
160
- files.push("src/styles.css");
161
-
162
- write(join(destination, ".env.example"), renderEnvExample(options.name));
163
- files.push(".env.example");
164
-
165
- write(join(destination, "docker-compose.yml"), renderDockerCompose());
166
- files.push("docker-compose.yml");
167
-
168
- await writeInitMigration(destination, options.features);
169
- files.push("kumiko/migrations/0001_init.sql", "kumiko/migrations/.snapshot.json");
170
-
171
- const deploy = scaffoldDeploy({ appName: options.name, destination });
172
- for (const f of deploy.files) {
173
- if (f.written) {
174
- files.push(relative(destination, f.path));
175
- }
176
- }
177
-
178
- if (testSetup !== undefined) {
179
- for (const [rel, content] of Object.entries(testSetup.files)) {
180
- mkdirSync(dirname(join(destination, rel)), { recursive: true });
181
- write(join(destination, rel), content);
182
- files.push(rel);
183
- }
184
- }
185
-
186
- write(
187
- join(destination, "README.md"),
188
- `${renderReadme(options.name, options.features)}${testSetup === undefined ? "" : `\n${testSetup.rulesMarkdown}`}`,
189
- );
190
- files.push("README.md");
191
-
192
- return { destination, files, appName: options.name };
193
- }
194
-
195
- // @wrapper-known semantic-alias
196
- function write(path: string, content: string): void {
197
- writeFileSync(path, content);
198
- }
199
-
200
- function renderPackageJson(
201
- name: string,
202
- version: string,
203
- testSetup: ScaffoldTestSetup | undefined,
204
- ): string {
205
- return `${JSON.stringify(
206
- {
207
- name,
208
- version: "0.1.0",
209
- private: true,
210
- type: "module",
211
- scripts: {
212
- dev: "bun --watch bin/dev.ts",
213
- build: "bun kumiko-build",
214
- start: "bun run bin/main.ts",
215
- boot: "KUMIKO_DRY_RUN_ENV=boot bun bin/main.ts",
216
- typecheck: "tsc --noEmit",
217
- lint: "biome check .",
218
- ...(testSetup?.scripts ?? { test: "bun --config=bunfig.ci.toml test --dots" }),
219
- "schema:apply": "bun kumiko-schema apply",
220
- "schema:generate": "bun kumiko-schema generate",
221
- },
222
- dependencies: {
223
- "@cosmicdrift/kumiko-bundled-features": version,
224
- "@cosmicdrift/kumiko-dev-server": version,
225
- "@cosmicdrift/kumiko-framework": version,
226
- "@cosmicdrift/kumiko-renderer-web": version,
227
- "@cosmicdrift/kumiko-server-runtime": version,
228
- react: "^19.2.6",
229
- "react-dom": "^19.2.6",
230
- zod: "^4.4.3",
231
- },
232
- devDependencies: sortedByName({
233
- "@biomejs/biome": "^2.4.15",
234
- "@tailwindcss/cli": "^4.3.0",
235
- "@types/react": "^19.2.0",
236
- "@types/react-dom": "^19.2.0",
237
- "bun-types": "^1.4.0",
238
- tailwindcss: "^4.3.0",
239
- typescript: "^6.0.3",
240
- ...testSetup?.devDependencies,
241
- }),
242
- },
243
- null,
244
- 2,
245
- )}\n`;
246
- }
247
-
248
- function sortedByName(record: Readonly<Record<string, string>>): Record<string, string> {
249
- return Object.fromEntries(Object.entries(record).sort(([a], [b]) => (a < b ? -1 : 1)));
250
- }
251
-
252
- function renderTsconfig(): string {
253
- return `${JSON.stringify(
254
- {
255
- compilerOptions: {
256
- strict: true,
257
- noUncheckedIndexedAccess: true,
258
- forceConsistentCasingInFileNames: true,
259
- verbatimModuleSyntax: true,
260
- target: "ESNext",
261
- module: "ESNext",
262
- moduleResolution: "bundler",
263
- esModuleInterop: true,
264
- skipLibCheck: true,
265
- lib: ["ESNext", "DOM"],
266
- types: ["bun-types"],
267
- jsx: "react-jsx",
268
- noEmit: true,
269
- },
270
- include: ["bin", "src", "kumiko", "e2e", "playwright.config.ts"],
271
- },
272
- null,
273
- 2,
274
- )}\n`;
275
- }
276
-
277
- function renderBiomeJson(): string {
278
- return `${JSON.stringify(
279
- {
280
- $schema: "https://biomejs.dev/schemas/2.4.15/schema.json",
281
- vcs: {
282
- enabled: true,
283
- clientKind: "git",
284
- useIgnoreFile: true,
285
- defaultBranch: "main",
286
- },
287
- files: {
288
- includes: [
289
- "src/**",
290
- "bin/**",
291
- "kumiko/**",
292
- "e2e/**",
293
- "playwright.config.ts",
294
- "!**/dist",
295
- "!kumiko/migrations",
296
- ],
297
- },
298
- formatter: {
299
- enabled: true,
300
- indentStyle: "space",
301
- indentWidth: 2,
302
- lineWidth: 100,
303
- lineEnding: "lf",
304
- },
305
- css: {
306
- parser: { cssModules: false, tailwindDirectives: true },
307
- },
308
- javascript: {
309
- formatter: {
310
- quoteStyle: "double",
311
- jsxQuoteStyle: "double",
312
- semicolons: "always",
313
- trailingCommas: "all",
314
- arrowParentheses: "always",
315
- },
316
- },
317
- json: { formatter: { indentWidth: 2, lineWidth: 80 } },
318
- linter: {
319
- enabled: true,
320
- rules: {
321
- recommended: true,
322
- correctness: { noUnusedVariables: "error", noUnusedImports: "error" },
323
- suspicious: { noExplicitAny: "error", noDebugger: "error", noConsole: "warn" },
324
- complexity: { useLiteralKeys: "off" },
325
- style: { useConst: "error" },
326
- nursery: { noFloatingPromises: "error" },
327
- },
328
- },
329
- overrides: [
330
- {
331
- includes: ["**/*.test.ts", "**/*.spec.ts", "**/*.integration.ts", "**/*.e2e.ts"],
332
- linter: {
333
- rules: {
334
- suspicious: { noConsole: "off" },
335
- style: { noNonNullAssertion: "off" },
336
- },
337
- },
338
- },
339
- ],
340
- },
341
- null,
342
- 2,
343
- )}\n`;
344
- }
345
-
346
- function renderBunfigToml(): string {
347
- return `[install]
348
- linker = "hoisted"
349
-
350
- [test]
351
- pathIgnorePatterns = [
352
- "**/e2e/**",
353
- "**/*.spec.ts",
354
- "**/dist/**",
355
- ]
356
- `;
357
- }
358
-
359
- function renderBunfigCiToml(): string {
360
- return `[install]
361
- linker = "hoisted"
362
-
363
- [test]
364
- pathIgnorePatterns = [
365
- "**/e2e/**",
366
- "**/*.spec.ts",
367
- "**/dist/**",
368
- ]
369
- `;
370
- }
371
-
372
- function newTsProject(): Project {
373
- return new Project({
374
- useInMemoryFileSystem: true,
375
- compilerOptions: { target: 99, module: 99, strict: true },
376
- manipulationSettings: { indentationText: IndentationText.TwoSpaces },
377
- });
378
- }
379
-
380
- const FOUNDATION_FEATURES: ReadonlyArray<ScaffoldFeatureEntry> = [
381
- {
382
- name: "secrets",
383
- importPath: "@cosmicdrift/kumiko-bundled-features/secrets",
384
- exportName: "createSecretsFeature",
385
- callExpression: "createSecretsFeature()",
386
- },
387
- // sessions' `sessionStore` extension requires auth-foundation to be mounted.
388
- {
389
- name: "auth-foundation",
390
- importPath: "@cosmicdrift/kumiko-bundled-features/auth-foundation",
391
- exportName: "authFoundationFeature",
392
- callExpression: "authFoundationFeature",
393
- },
394
- {
395
- name: "sessions",
396
- importPath: "@cosmicdrift/kumiko-bundled-features/sessions",
397
- exportName: "createSessionsFeature",
398
- callExpression: "createSessionsFeature()",
399
- },
400
- ];
401
-
402
- // composeFeatures({ includeBundled: true }) auto-mountet diese 4 Foundation-
403
- // Features. Sie hier nochmal in APP_FEATURES zu schreiben löste den dedupe-
404
- // warn-Spam im scaffolded `bun dev` aus (PR #599 hat den createRegistry-
405
- // Crash gefangen, der Spam blieb bis hier). Filter wirkt defensiv: auch wenn
406
- // jemand scaffoldApp() direkt mit allen Bundled-Feature-Entries aufruft
407
- // rutschen die 4 nicht in run-config.ts.
408
- const COMPOSE_AUTO_MOUNTED_NAMES = new Set(["config", "user", "tenant", "auth-email-password"]);
409
-
410
- function renderRunConfig(features?: ReadonlyArray<ScaffoldFeatureEntry>): string {
411
- const project = newTsProject();
412
- const sf = project.createSourceFile("run-config.ts", "");
413
-
414
- // Fallback decided BEFORE filtering: if the caller passed features (even
415
- // if every one of them is an auto-mounted name), an all-filtered-out empty
416
- // result is the correct outcome — falling back to FOUNDATION_FEATURES here
417
- // would silently substitute a different feature set than the caller asked
418
- // for.
419
- const base = features?.length ? features : FOUNDATION_FEATURES;
420
- const effective = base.filter((f) => !COMPOSE_AUTO_MOUNTED_NAMES.has(f.name));
421
- const grouped = new Map<string, string[]>();
422
- for (const entry of effective) {
423
- const existing = grouped.get(entry.importPath) ?? [];
424
- if (!existing.includes(entry.exportName)) existing.push(entry.exportName);
425
- grouped.set(entry.importPath, existing);
426
- }
427
- for (const [importPath, namedImports] of grouped) {
428
- sf.addImportDeclaration({ moduleSpecifier: importPath, namedImports });
429
- }
430
- sf.addImportDeclaration({
431
- moduleSpecifier: "./features/tasks",
432
- namedImports: ["tasksFeature"],
433
- });
434
-
435
- const callExprs = [...effective.map((f) => f.callExpression), "tasksFeature"];
436
- sf.addVariableStatement({
437
- declarationKind: VariableDeclarationKind.Const,
438
- isExported: true,
439
- declarations: [
440
- {
441
- name: "APP_FEATURES",
442
- initializer: `[${callExprs.join(", ")}] as const`,
443
- },
444
- ],
445
- });
446
-
447
- sf.addVariableStatement({
448
- declarationKind: VariableDeclarationKind.Const,
449
- isExported: true,
450
- declarations: [{ name: "HAS_AUTH", initializer: "true" }],
451
- });
452
-
453
- sf.insertText(
454
- 0,
455
- [
456
- "// Single source of truth for your app's feature composition.",
457
- "// config/user/tenant/auth-email-password are added automatically",
458
- "// via composeFeatures(includeBundled:true) when runProdApp is called",
459
- "// with `auth: {…}` (see bin/main.ts).",
460
- "//",
461
- "// Add new features:",
462
- "// - bunx @cosmicdrift/kumiko-cli add feature <name> (DX-2, automatic)",
463
- "// - or: hand-edit + add the import below",
464
- "",
465
- "",
466
- ].join("\n"),
467
- );
468
-
469
- return sf.getFullText();
470
- }
471
-
472
- function renderMain(appName: string): string {
473
- const tenantId = deriveTenantId(appName);
474
- const project = newTsProject();
475
- const sf = project.createSourceFile("main.ts", "");
476
-
477
- sf.addImportDeclaration({
478
- moduleSpecifier: "@cosmicdrift/kumiko-dev-server/env-schema",
479
- namedImports: ["frameworkCoreEnvSchema"],
480
- });
481
- sf.addImportDeclaration({
482
- moduleSpecifier: "@cosmicdrift/kumiko-server-runtime",
483
- namedImports: ["composeFeatures", "runProdApp"],
484
- });
485
- sf.addImportDeclaration({
486
- moduleSpecifier: "@cosmicdrift/kumiko-framework/engine",
487
- isTypeOnly: true,
488
- namedImports: ["TenantId"],
489
- });
490
- sf.addImportDeclaration({
491
- moduleSpecifier: "@cosmicdrift/kumiko-framework/env",
492
- namedImports: ["composeEnvSchema"],
493
- });
494
- sf.addImportDeclaration({
495
- moduleSpecifier: "@cosmicdrift/kumiko-framework/crypto",
496
- namedImports: ["requireKmsWiring", "resolveKmsWiring"],
497
- });
498
- sf.addImportDeclaration({
499
- moduleSpecifier: "../src/run-config",
500
- namedImports: ["APP_FEATURES", "HAS_AUTH"],
501
- });
502
-
503
- sf.addVariableStatement({
504
- declarationKind: VariableDeclarationKind.Const,
505
- declarations: [
506
- {
507
- name: "DEFAULT_TENANT_ID",
508
- initializer: `"${tenantId}" as TenantId`,
509
- },
510
- ],
511
- });
512
-
513
- // The envSchema must cover the SAME features runProdApp mounts at boot.
514
- // `auth: { admin: … }` below makes runProdApp auto-mix config/user/tenant/
515
- // auth-email-password via composeFeatures(includeBundled:HAS_AUTH); compose the
516
- // identical set here so the auth feature's JWT_SECRET (min-32) declaration
517
- // is part of the boot-gate — otherwise a too-short JWT_SECRET slips through.
518
- sf.addVariableStatement({
519
- declarationKind: VariableDeclarationKind.Const,
520
- declarations: [
521
- {
522
- name: "bootFeatures",
523
- initializer: "composeFeatures(APP_FEATURES, { includeBundled: HAS_AUTH })",
524
- },
525
- ],
526
- });
527
-
528
- sf.addVariableStatement({
529
- declarationKind: VariableDeclarationKind.Const,
530
- declarations: [
531
- {
532
- name: "envSchema",
533
- initializer: "composeEnvSchema({ core: frameworkCoreEnvSchema, features: bootFeatures })",
534
- },
535
- ],
536
- });
537
-
538
- // Subject-key KMS for the pii-annotated entities the --yes recommended set
539
- // mounts (user, tenant-invitation, fileRef). Local/dev may omit the trio
540
- // (plaintext + loud warning); NODE_ENV=production uses requireKmsWiring so
541
- // a real deploy cannot silently store plaintext PII (fw#2317 / #2339).
542
- // assertPiiBootInvariants already warns on the plaintext path — no duplicate console.warn.
543
- sf.addVariableStatement({
544
- declarationKind: VariableDeclarationKind.Const,
545
- declarations: [
546
- {
547
- name: "kmsOpts",
548
- initializer: (writer) => {
549
- writer.write("{").newLine();
550
- writer.writeLine(` logPrefix: "[${appName}]",`);
551
- writer.writeLine(
552
- ` plaintextReason: "no PLATFORM_KEK / SUBJECT_KEYS_DATABASE_URL / KUMIKO_BLIND_INDEX_KEY set — see .env.example",`,
553
- );
554
- writer.write("} as const");
555
- },
556
- },
557
- ],
558
- });
559
- sf.addVariableStatement({
560
- declarationKind: VariableDeclarationKind.Const,
561
- declarations: [
562
- {
563
- name: "kmsWiring",
564
- initializer: (writer) => {
565
- writer.write(`process.env["NODE_ENV"] === "production"`);
566
- writer.newLine();
567
- writer.write(" ? requireKmsWiring(process.env, kmsOpts)");
568
- writer.newLine();
569
- writer.write(" : resolveKmsWiring(process.env, kmsOpts)");
570
- },
571
- },
572
- ],
573
- });
574
-
575
- sf.addStatements((writer) => {
576
- writer
577
- .write("await runProdApp(")
578
- .inlineBlock(() => {
579
- writer.writeLine("features: APP_FEATURES,");
580
- writer.writeLine("envSchema,");
581
- writer.writeLine('staticDir: "./dist",');
582
- writer.writeLine("...kmsWiring,");
583
- writer.write("auth: ").inlineBlock(() => {
584
- writer.write("admin: ").inlineBlock(() => {
585
- writer.writeLine(`email: "admin@${appName}.local",`);
586
- writer.writeLine(`password: "change-me-on-first-deploy",`);
587
- writer.writeLine(`displayName: "Admin",`);
588
- writer.write("memberships: [");
589
- writer.indent(() => {
590
- writer.inlineBlock(() => {
591
- writer.writeLine("tenantId: DEFAULT_TENANT_ID,");
592
- writer.writeLine(`tenantKey: "${appName}",`);
593
- writer.writeLine(`tenantName: "${appName}",`);
594
- writer.writeLine(`roles: ["TenantAdmin"],`);
595
- });
596
- writer.write(",");
597
- });
598
- writer.write("],");
599
- });
600
- });
601
- })
602
- .write(");");
603
- });
604
-
605
- sf.insertText(
606
- 0,
607
- [
608
- "// Production-bootstrap. KUMIKO_DRY_RUN_ENV=boot exits after",
609
- "// composeFeatures + validateBoot + createRegistry without DB/Redis-connect",
610
- "// (see @cosmicdrift/kumiko-server-runtime runProdApp). The real dev boot",
611
- "// runs via `bunx kumiko dev` (in-repo dev-tool) with a Docker stack — DX-1.0",
612
- "// only covers the boot-mode path; `kumiko dev` lands in a later DX phase.",
613
- "",
614
- "",
615
- ].join("\n"),
616
- );
617
-
618
- return sf.getFullText();
619
- }
620
-
621
- function renderDev(appName: string): string {
622
- const tenantId = deriveTenantId(appName);
623
- const project = newTsProject();
624
- const sf = project.createSourceFile("dev.ts", "");
625
-
626
- sf.addImportDeclaration({
627
- moduleSpecifier: "@cosmicdrift/kumiko-dev-server",
628
- namedImports: ["runDevApp"],
629
- });
630
- sf.addImportDeclaration({
631
- moduleSpecifier: "@cosmicdrift/kumiko-framework/engine",
632
- isTypeOnly: true,
633
- namedImports: ["TenantId"],
634
- });
635
- sf.addImportDeclaration({
636
- moduleSpecifier: "../src/run-config",
637
- namedImports: ["APP_FEATURES"],
638
- });
639
- sf.addImportDeclaration({
640
- moduleSpecifier: "../src/seed",
641
- namedImports: ["seedDemoTasks"],
642
- });
643
-
644
- sf.addVariableStatement({
645
- declarationKind: VariableDeclarationKind.Const,
646
- declarations: [
647
- {
648
- name: "DEFAULT_TENANT_ID",
649
- initializer: `"${tenantId}" as TenantId`,
650
- },
651
- ],
652
- });
653
-
654
- sf.addStatements((writer) => {
655
- writer
656
- .write("await runDevApp(")
657
- .inlineBlock(() => {
658
- writer.writeLine("features: APP_FEATURES,");
659
- writer.writeLine("welcomeBanner: true,");
660
- writer.writeLine(`clientEntry: "./src/client.tsx",`);
661
- writer.writeLine("seeds: [seedDemoTasks],");
662
- writer.write("auth: ").inlineBlock(() => {
663
- writer.write("admin: ").inlineBlock(() => {
664
- writer.writeLine(`email: "admin@${appName}.local",`);
665
- writer.writeLine(`password: "changeme",`);
666
- writer.writeLine(`displayName: "Admin",`);
667
- // Dev-only: skip the email-verify login gate for the seeded
668
- // admin. Prod's scaffold (renderMain) leaves this unset on
669
- // purpose — the verify flow must run there.
670
- writer.writeLine("emailVerified: true,");
671
- writer.write("memberships: [");
672
- writer.indent(() => {
673
- writer.inlineBlock(() => {
674
- writer.writeLine("tenantId: DEFAULT_TENANT_ID,");
675
- writer.writeLine(`tenantKey: "${appName}",`);
676
- writer.writeLine(`tenantName: "${appName}",`);
677
- writer.writeLine(`roles: ["TenantAdmin"],`);
678
- });
679
- writer.write(",");
680
- });
681
- writer.write("],");
682
- });
683
- });
684
- })
685
- .write(");");
686
- });
687
-
688
- sf.insertText(
689
- 0,
690
- [
691
- "// Dev bootstrap. `bun --watch bin/dev.ts` (see package.json scripts.dev)",
692
- "// starts a full-featured dev server with auto-reload on code changes.",
693
- "// setupTestStack creates missing entity tables automatically — a new",
694
- "// r.entity(...) in a feature becomes a CREATE TABLE on the next reboot,",
695
- "// no manual `kumiko schema apply` needed (that's prod-only).",
696
- "// Persistent DB via KUMIKO_DEV_DB_NAME (.env) so admin + data survive reboots.",
697
- "",
698
- "",
699
- ].join("\n"),
700
- );
701
-
702
- return sf.getFullText();
703
- }
704
-
705
- function renderClient(appName: string): string {
706
- return [
707
- "// Browser entry. runDevApp's clientEntry option bundles this file to",
708
- "// /client.js and the default HTML loads it. createKumikoApp fetches the",
709
- "// AppSchema itself from the authenticated GET /api/schema and mounts",
710
- "// the routes.",
711
- "//",
712
- "// AppShell wraps DefaultAppShell to supply `brand` — createKumikoApp's",
713
- "// shell option only injects schema + children. Without `shell` the",
714
- "// active screen renders with no layout wrapper (a bare banner after",
715
- "// login instead of the app). emailPasswordClient() brings the login",
716
- "// screen + session provider — without it /login stays empty.",
717
- "//",
718
- "// Add new client plugins (e.g. notificationsClient()) to clientFeatures",
719
- "// here — symmetric to APP_FEATURES on the server side.",
720
- "",
721
- 'import { emailPasswordClient } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/web";',
722
- 'import { tasksClient } from "./features/tasks/web";',
723
- 'import { type AppSchema, createKumikoApp, DefaultAppShell } from "@cosmicdrift/kumiko-renderer-web";',
724
- 'import type { ReactNode } from "react";',
725
- "",
726
- "function AppShell({ children, schema }: { children: ReactNode; schema: AppSchema }): ReactNode {",
727
- " return (",
728
- // html-ok: appName is kebab-validated (a-z/0-9/-); this is generated .tsx source, not runtime HTML.
729
- ` <DefaultAppShell brand={<span className="font-semibold tracking-tight">${appName}</span>} schema={schema}>`,
730
- " {children}",
731
- " </DefaultAppShell>",
732
- " );",
733
- "}",
734
- "",
735
- "createKumikoApp({",
736
- " shell: AppShell,",
737
- " clientFeatures: [emailPasswordClient(), tasksClient],",
738
- "});",
739
- "",
740
- ].join("\n");
741
- }
742
-
743
- function renderEnvExample(appName: string): string {
744
- const devDb = `${appName.replace(/-/g, "_")}_dev`;
745
- return `# bun dev (runDevApp → setupTestStack) needs TEST_DATABASE_URL.
746
- # Production (bun bin/main.ts → runProdApp) needs DATABASE_URL.
747
- # Both default to the same local Postgres — runDevApp creates its own
748
- # "<KUMIKO_DEV_DB_NAME>" database underneath.
749
- TEST_DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/postgres
750
- DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/app
751
- REDIS_URL=redis://127.0.0.1:6379
752
-
753
- # JWT_SECRET: min 32 chars. Generate with: openssl rand -base64 32
754
- JWT_SECRET=change-me-min-32-chars-change-me-min-32
755
-
756
- # KUMIKO_SECRETS_MASTER_KEY_V1: base64-encoded 32 bytes (AES-256 KEK).
757
- # Generate with: openssl rand -base64 32
758
- KUMIKO_SECRETS_MASTER_KEY_V1=
759
-
760
- # Subject-keys KMS for PII fields (user, tenant-invitation, fileRef). All-or-none.
761
- # Unset → local boot with plaintext PII + warning; NODE_ENV=production requires
762
- # the trio (requireKmsWiring). Generate PLATFORM_KEK / KUMIKO_BLIND_INDEX_KEY
763
- # with: openssl rand -base64 32. SUBJECT_KEYS_DATABASE_URL must be a dedicated
764
- # subject-keys cluster (not the app DB) — see kms-adapter.md.
765
- PLATFORM_KEK=
766
- # Separate Postgres instance for subject keys — never DATABASE_URL (retention
767
- # must outlive app-DB backups for crypto-shredding).
768
- SUBJECT_KEYS_DATABASE_URL=
769
- KUMIKO_BLIND_INDEX_KEY=
770
-
771
- # Dev-only: persistent DB for \`bun dev\`. Without this var every reboot starts
772
- # a fresh kumiko_test_<random> DB → admin login + data gone on every edit.
773
- # With it the DB persists across reboots (schema pushes are idempotent).
774
- KUMIKO_DEV_DB_NAME=${devDb}
775
- `;
776
- }
777
-
778
- // Ports + credentials match the *_URL defaults in renderEnvExample, so
779
- // `docker compose up -d` just works with the generated .env. Named pg volume
780
- // so dev data survives `docker compose down` (pairs with KUMIKO_DEV_DB_NAME
781
- // persistence) — the loopback-binding rationale is in the generated file's
782
- // own comment (657/1), no need to duplicate it here.
783
- function renderDockerCompose(): string {
784
- return `# Local Postgres + Redis for \`bun dev\`. Matches the *_URL defaults in .env.example.
785
- # Start: docker compose up -d · Stop: docker compose down · Reset: docker compose down -v
786
- # Ports bind to 127.0.0.1 only — weak dev credentials must not be exposed on the LAN.
787
- services:
788
- postgres:
789
- # Pinned to the project's own compose-file tag (663/1) — Alpine variant
790
- # (~90MB vs ~400MB) and a reproducible patch version, bump on PG18 minors.
791
- image: postgres:18.3-alpine
792
- environment:
793
- POSTGRES_USER: postgres
794
- POSTGRES_PASSWORD: postgres
795
- ports:
796
- - "127.0.0.1:5432:5432"
797
- volumes:
798
- - kumiko-pg:/var/lib/postgresql
799
- redis:
800
- image: redis:7
801
- ports:
802
- - "127.0.0.1:6379:6379"
803
- volumes:
804
- kumiko-pg:
805
- `;
806
- }
807
-
808
- function renderReadme(
809
- appName: string,
810
- features: ReadonlyArray<ScaffoldFeatureEntry> | undefined,
811
- ): string {
812
- const featureList =
813
- features && features.length > 0
814
- ? [...features.map((f) => `- \`${f.name}\``), "- `tasks` (demo — list + edit screens)"].join(
815
- "\n",
816
- )
817
- : "- `secrets` (foundation)\n- `sessions` (foundation)\n- `tasks` (demo — list + edit screens)";
818
- return `# ${appName}
819
-
820
- Scaffolded by \`kumiko new app\`. Includes a demo **tasks** feature with list +
821
- edit screens, sidebar nav, and seeded rows — \`bun dev\` shows a working admin UI
822
- after login. Add more features via \`bunx @cosmicdrift/kumiko-cli add feature <name>\`.
823
-
824
- ## Mounted features
825
-
826
- ${featureList}
827
-
828
- Edit \`src/run-config.ts\` to add bundled features. The demo lives in
829
- \`src/features/tasks/\`.
830
-
831
- ## First run (browser)
832
-
833
- \`\`\`sh
834
- bun install
835
- cp .env.example .env
836
- # set JWT_SECRET + KUMIKO_SECRETS_MASTER_KEY_V1 in .env
837
- docker compose up -d # local Postgres + Redis (skip if you already have them)
838
- bun dev
839
- \`\`\`
840
-
841
- The welcome banner prints the URL (default \`http://localhost:4173\`) and admin
842
- login. Sign in as \`admin@${appName}.local\` / \`changeme\`, then open **Tasks**
843
- in the sidebar — demo rows are pre-seeded.
844
-
845
- ## Boot-only smoke (no DB needed)
846
-
847
- \`\`\`sh
848
- bun run boot
849
- \`\`\`
850
-
851
- Runs \`KUMIKO_DRY_RUN_ENV=boot bun bin/main.ts\` — validates feature composition
852
- + env schema, exits 0 without touching DB/Redis. Useful in CI.
853
-
854
- Without \`PLATFORM_KEK\` / \`SUBJECT_KEYS_DATABASE_URL\` / \`KUMIKO_BLIND_INDEX_KEY\`
855
- set (see \`.env.example\`), both \`bun run boot\` and \`bun run start\` boot with
856
- PII fields (user, tenant-invitation, fileRef) stored in plaintext and a loud
857
- warning — set all three before a real deploy.
858
-
859
- ## Production build + schema
860
-
861
- \`\`\`sh
862
- bun run build # kumiko-build → dist/ + dist-server/
863
- bun run schema:apply # apply checked-in kumiko/migrations (needs DATABASE_URL)
864
- bun run start # runProdApp against dist/
865
- \`\`\`
866
-
867
- After adding entities/features, regenerate migrations:
868
-
869
- \`\`\`sh
870
- bun run schema:generate <name>
871
- \`\`\`
872
-
873
- ## Deploy
874
-
875
- \`deploy/Dockerfile\` + \`deploy/migrate-step.sh\` are scaffolded for container
876
- deploys. Build context = app repo root; migrations ship in \`kumiko/migrations/\`.
877
-
878
- ## Architecture
879
-
880
- - \`src/run-config.ts\` — single source of truth: which features your app mounts (\`APP_FEATURES\`, \`HAS_AUTH\`).
881
- - \`src/features/tasks/\` — demo feature (entity + handlers + screens + nav).
882
- - \`src/seed.ts\` — dev seed for demo tasks (\`bun dev\` only).
883
- - \`kumiko/schema.ts\` — same feature set → \`ENTITY_METAS\` for \`kumiko schema\`.
884
- - \`bin/dev.ts\` — dev-server entry (\`bun dev\`).
885
- - \`bin/main.ts\` — production-bootstrap (\`bun run start\`).
886
- - \`bin/kumiko.ts\` — schema + consumer-ops CLI bundled into \`dist-server/kumiko.js\`.
887
- - \`docker-compose.yml\` — local Postgres + Redis for \`bun dev\`.
888
-
889
- For full docs see https://docs.kumiko.rocks.
890
- `;
891
- }
892
-
893
- function renderStylesCss(): string {
894
- return [
895
- '@import "@cosmicdrift/kumiko-renderer-web/styles.css";',
896
- "",
897
- '@source "./**/*.{ts,tsx}";',
898
- "",
899
- ].join("\n");
900
- }
901
-
902
- function renderKumikoSchema(): string {
903
- return [
904
- "// Live ENTITY_METAS source for `kumiko schema generate|apply|status`.",
905
- "//",
906
- "// Computes table-metas from the SAME composeFeatures(APP_FEATURES) the",
907
- "// runtime sees (runProdApp/runDevApp) — migration and runtime cannot drift.",
908
- "",
909
- 'import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";',
910
- 'import { collectTableMetas, type EntityTableMeta } from "@cosmicdrift/kumiko-framework/db";',
911
- 'import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";',
912
- 'import { APP_FEATURES, HAS_AUTH } from "../src/run-config";',
913
- "",
914
- "export const FEATURES: readonly FeatureDefinition[] = composeFeatures([...APP_FEATURES], {",
915
- " includeBundled: HAS_AUTH,",
916
- "});",
917
- "",
918
- "export const ENTITY_METAS: readonly EntityTableMeta[] = collectTableMetas(FEATURES);",
919
- "",
920
- ].join("\n");
921
- }
922
-
923
- function renderBinKumiko(): string {
924
- return [
925
- "#!/usr/bin/env bun",
926
- "",
927
- "// Standalone kumiko CLI for the production bundle. The deploy migrate-step",
928
- "// runs `bun /app/kumiko.js schema apply`; ops runs `bun /app/kumiko.js",
929
- "// consumer status|restart <name>` to recover a dead event consumer without",
930
- "// ad-hoc SQL. kumiko-build bundles this file to dist-server/kumiko.js.",
931
- "",
932
- 'import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";',
933
- 'import { runConsumerCli } from "@cosmicdrift/kumiko-framework/consumer-cli";',
934
- 'import { runSchemaCli } from "@cosmicdrift/kumiko-framework/schema-cli";',
935
- 'import { APP_FEATURES, HAS_AUTH } from "../src/run-config";',
936
- "",
937
- "const [, , cmd, ...rest] = Bun.argv;",
938
- "// biome-ignore lint/suspicious/noConsole: CLI output is the feature.",
939
- "const out = { log: (l: string) => console.log(l), err: (l: string) => console.error(l) };",
940
- "",
941
- 'if (cmd === "schema") {',
942
- " const features = composeFeatures([...APP_FEATURES], { includeBundled: HAS_AUTH });",
943
- " process.exit(await runSchemaCli(rest, process.env.INIT_CWD ?? process.cwd(), out, { features }));",
944
- "}",
945
- "",
946
- 'if (cmd === "consumer") {',
947
- " process.exit(await runConsumerCli(rest, out));",
948
- "}",
949
- "",
950
- "// biome-ignore lint/suspicious/noConsole: CLI output is the feature.",
951
- 'console.error("\\n Unknown: kumiko " + (cmd ?? "") + " — only \'kumiko schema <sub>\' or \'kumiko consumer <sub>\' in the standalone bundle.\\n");',
952
- "process.exit(1);",
953
- "",
954
- ].join("\n");
955
- }
956
-
957
- async function instantiateScaffoldFeatures(
958
- features?: ReadonlyArray<ScaffoldFeatureEntry>,
959
- ): Promise<readonly FeatureDefinition[]> {
960
- const base = features?.length ? features : FOUNDATION_FEATURES;
961
- const effective = base.filter((f) => !COMPOSE_AUTO_MOUNTED_NAMES.has(f.name));
962
- const instances: FeatureDefinition[] = [];
963
- for (const entry of effective) {
964
- const mod = (await import(entry.importPath)) as Record<string, unknown>;
965
- const exp = mod[entry.exportName];
966
- if (exp === undefined) {
967
- throw new Error(
968
- `scaffoldApp: ${entry.importPath} missing export ${entry.exportName} for ${entry.callExpression}`,
969
- );
970
- }
971
- const looksCallable = entry.callExpression.endsWith(")");
972
- if (looksCallable !== (typeof exp === "function")) {
973
- throw new Error(
974
- `scaffoldApp: ${entry.importPath} export ${entry.exportName} is ${
975
- typeof exp === "function" ? "" : "not "
976
- }callable but callExpression "${entry.callExpression}" says otherwise`,
977
- );
978
- }
979
- if (typeof exp === "function") {
980
- instances.push((exp as (...args: unknown[]) => FeatureDefinition)(...(entry.callArgs ?? [])));
981
- } else {
982
- instances.push(exp as FeatureDefinition);
983
- }
984
- }
985
- instances.push(createDemoTasksFeature());
986
- return instances;
987
- }
988
-
989
- async function writeInitMigration(
990
- destination: string,
991
- features?: ReadonlyArray<ScaffoldFeatureEntry>,
992
- ): Promise<void> {
993
- const instances = await instantiateScaffoldFeatures(features);
994
- const composed = composeFeatures(instances, { includeBundled: true });
995
- const metas = collectTableMetas(composed);
996
- const result = generateMigration({
997
- metas,
998
- prevSnapshot: null,
999
- name: "init",
1000
- sequenceNumber: 1,
1001
- });
1002
- const migrationsDir = join(destination, "kumiko", "migrations");
1003
- mkdirSync(migrationsDir, { recursive: true });
1004
- writeFileSync(join(migrationsDir, result.filename), result.sqlContent);
1005
- writeSnapshotJson(join(migrationsDir, ".snapshot.json"), result.snapshot);
1006
- }
1007
-
1008
- // Deterministic tenant-ID from app-name. Format: UUID-v4 with the
1009
- // version-marker at the right spot. NOT cryptographically random —
1010
- // just a stable per-app default the user can change later.
1011
- function deriveTenantId(name: string): string {
1012
- let state = 2166136261;
1013
- for (const ch of name) {
1014
- state ^= ch.charCodeAt(0);
1015
- state = Math.imul(state, 16777619) >>> 0;
1016
- }
1017
- // @wrapper-known semantic-alias
1018
- const hex = (n: number, len: number): string => n.toString(16).padStart(len, "0").slice(0, len);
1019
- const a = hex(state, 8);
1020
- state ^= state << 13;
1021
- state >>>= 0;
1022
- const b = hex(state, 4);
1023
- state ^= state >>> 17;
1024
- state >>>= 0;
1025
- const c = `4${hex(state, 3)}`;
1026
- state ^= state << 5;
1027
- state >>>= 0;
1028
- const d4 = (0x8 | (state & 0x3)).toString(16);
1029
- const d = `${d4}${hex(state >>> 4, 3)}`;
1030
- state = Math.imul(state, 16777619) >>> 0;
1031
- const e = hex(state, 12);
1032
- return `${a}-${b}-${c}-${d}-${e}`;
1033
- }