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