@cosmicdrift/kumiko-dev-server 0.329.0 → 0.330.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/bin/kumiko-build.ts +3 -2
- package/bin/kumiko-dev.ts +1 -1
- package/bin/kumiko-init-deploy.ts +1 -1
- package/bin/kumiko-schema-check.ts +4 -1
- package/dist/build-server-bundle.d.ts +31 -0
- package/dist/build-server-bundle.js +282 -0
- package/dist/build.d.ts +2 -0
- package/dist/build.js +5 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +9 -0
- package/dist/codegen/index.d.ts +4 -0
- package/dist/codegen/index.js +4 -0
- package/dist/codegen/render.d.ts +55 -0
- package/dist/codegen/render.js +273 -0
- package/dist/codegen/run-codegen.d.ts +20 -0
- package/dist/codegen/run-codegen.js +219 -0
- package/dist/codegen/scan-events.d.ts +76 -0
- package/dist/codegen/scan-events.js +447 -0
- package/dist/codegen/watch.d.ts +33 -0
- package/dist/codegen/watch.js +112 -0
- package/dist/compose-stacks.d.ts +60 -0
- package/dist/compose-stacks.js +127 -0
- package/dist/crash-tracker.d.ts +17 -0
- package/dist/crash-tracker.js +37 -0
- package/dist/create-kumiko-server.d.ts +195 -0
- package/dist/create-kumiko-server.js +1040 -0
- package/dist/env-schema.d.ts +15 -0
- package/dist/env-schema.js +60 -0
- package/dist/few-shot-corpus.d.ts +72 -0
- package/dist/few-shot-corpus.js +262 -0
- package/dist/framework-version.d.ts +1 -0
- package/dist/framework-version.js +31 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +33 -0
- package/dist/init-deploy-cli.d.ts +10 -0
- package/dist/init-deploy-cli.js +111 -0
- package/dist/kebab.d.ts +1 -0
- package/{src/kebab.ts → dist/kebab.js} +2 -3
- package/dist/run-dev-app.d.ts +174 -0
- package/dist/run-dev-app.js +403 -0
- package/dist/scaffold-app-feature.d.ts +16 -0
- package/dist/scaffold-app-feature.js +240 -0
- package/dist/scaffold-app.d.ts +45 -0
- package/dist/scaffold-app.js +852 -0
- package/dist/scaffold-demo-tasks.d.ts +8 -0
- package/{src/scaffold-demo-tasks.ts → dist/scaffold-demo-tasks.js} +69 -98
- package/dist/scaffold-deploy.d.ts +92 -0
- package/dist/scaffold-deploy.js +254 -0
- package/dist/scaffold-feature.d.ts +26 -0
- package/dist/scaffold-feature.js +163 -0
- package/dist/schema-apply.d.ts +11 -0
- package/dist/schema-apply.js +95 -0
- package/dist/schema-check-core.d.ts +2 -0
- package/{src/schema-check-core.ts → dist/schema-check-core.js} +8 -10
- package/dist/schema-tables-auth-mode.d.ts +2 -0
- package/{src/schema-tables-auth-mode.ts → dist/schema-tables-auth-mode.js} +2 -10
- package/dist/schema-tables-minimal.d.ts +2 -0
- package/{src/schema-tables-minimal.ts → dist/schema-tables-minimal.js} +2 -11
- package/dist/setup-test-stack-from-features.d.ts +11 -0
- package/dist/setup-test-stack-from-features.js +42 -0
- package/dist/welcome-banner.d.ts +15 -0
- package/dist/welcome-banner.js +27 -0
- package/package.json +71 -24
- package/src/changes.json +6 -0
- package/src/__tests__/build-prod-bundle.integration.test.ts +0 -282
- package/src/__tests__/build-server-bundle.test.ts +0 -228
- package/src/__tests__/classify-change.test.ts +0 -87
- package/src/__tests__/compose-stacks.test.ts +0 -273
- package/src/__tests__/config-resolver-default.integration.test.ts +0 -165
- package/src/__tests__/crash-tracker.test.ts +0 -89
- package/src/__tests__/create-kumiko-server-errors.test.ts +0 -54
- package/src/__tests__/create-kumiko-server-options.test.ts +0 -33
- package/src/__tests__/create-kumiko-server.integration.test.ts +0 -1007
- package/src/__tests__/discover-format.test.ts +0 -32
- package/src/__tests__/env-schema.integration.test.ts +0 -248
- package/src/__tests__/env-schema.test.ts +0 -132
- package/src/__tests__/few-shot-corpus.test.ts +0 -316
- package/src/__tests__/framework-version.test.ts +0 -45
- package/src/__tests__/init-deploy-cli.test.ts +0 -113
- package/src/__tests__/kumiko-schema-check.test.ts +0 -56
- package/src/__tests__/merge-extra-context.test.ts +0 -67
- package/src/__tests__/resolve-stylesheet.test.ts +0 -132
- package/src/__tests__/run-dev-app-boot-validation.test.ts +0 -38
- package/src/__tests__/run-dev-app.integration.test.ts +0 -290
- package/src/__tests__/runtime-classification.test.ts +0 -26
- package/src/__tests__/saas-identity-wire.integration.test.ts +0 -427
- package/src/__tests__/scaffold-app-feature.test.ts +0 -216
- package/src/__tests__/scaffold-app.test.ts +0 -630
- package/src/__tests__/scaffold-demo-tasks.test.ts +0 -47
- package/src/__tests__/scaffold-deploy.test.ts +0 -464
- package/src/__tests__/scaffold-dev-cycle.integration.test.ts +0 -109
- package/src/__tests__/scaffold-feature.test.ts +0 -143
- package/src/__tests__/schema-apply.integration.test.ts +0 -191
- package/src/__tests__/setup-test-stack-from-features.integration.test.ts +0 -30
- package/src/__tests__/setup-test-stack-job-run-logger.integration.test.ts +0 -72
- package/src/__tests__/walkthrough.integration.test.ts +0 -140
- package/src/__tests__/welcome-banner.test.ts +0 -42
- package/src/build-server-bundle.ts +0 -346
- package/src/build.ts +0 -28
- package/src/codegen/__tests__/render-codegen.test.ts +0 -100
- package/src/codegen/__tests__/run-codegen.test.ts +0 -665
- package/src/codegen/__tests__/strict-mode-diagnostics.test.ts +0 -347
- package/src/codegen/__tests__/watch.test.ts +0 -222
- package/src/codegen/index.ts +0 -19
- package/src/codegen/render.ts +0 -305
- package/src/codegen/run-codegen.ts +0 -278
- package/src/codegen/scan-events.ts +0 -565
- package/src/codegen/watch.ts +0 -140
- package/src/compose-stacks.ts +0 -193
- package/src/crash-tracker.ts +0 -56
- package/src/create-kumiko-server.ts +0 -1397
- package/src/env-schema.ts +0 -72
- package/src/few-shot-corpus.ts +0 -370
- package/src/framework-version.ts +0 -39
- package/src/index.ts +0 -105
- package/src/init-deploy-cli.ts +0 -148
- package/src/run-dev-app.ts +0 -694
- package/src/scaffold-app-feature.ts +0 -283
- package/src/scaffold-app.ts +0 -1033
- package/src/scaffold-deploy.ts +0 -393
- package/src/scaffold-feature.ts +0 -226
- package/src/schema-apply.ts +0 -123
- package/src/setup-test-stack-from-features.ts +0 -80
- package/src/welcome-banner.ts +0 -43
package/src/scaffold-app.ts
DELETED
|
@@ -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
|
-
}
|