@cosmicdrift/kumiko-dev-server 0.364.0 → 0.365.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-server-bundle.js +12 -9
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +0 -1
- package/dist/compose-stacks.d.ts +8 -1
- package/dist/compose-stacks.js +6 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/resolve-dev-pii-kms.d.ts +2 -0
- package/dist/resolve-dev-pii-kms.js +16 -3
- package/dist/run-dev-app.js +9 -6
- package/dist/scaffold-app-feature.js +1 -2
- package/dist/scaffold-app.d.ts +1 -0
- package/dist/scaffold-app.js +270 -295
- package/dist/scaffold-demo-tasks.js +2 -1
- package/package.json +8 -9
- package/src/changes.json +37 -0
- package/bin/kumiko-schema-check.ts +0 -175
- package/dist/schema-check-core.d.ts +0 -2
- package/dist/schema-check-core.js +0 -24
|
@@ -110,9 +110,9 @@ export async function buildServerBundle(options = {}) {
|
|
|
110
110
|
await mkdir(outDir, { recursive: true });
|
|
111
111
|
// Ein Bun.build-Call mit allen Entries + splitting → das Framework wird
|
|
112
112
|
// einmal als shared chunk abgelegt statt pro Entry inlined.
|
|
113
|
-
const optionalSiblingEntries = ["worker
|
|
114
|
-
.map((
|
|
115
|
-
.filter((path) =>
|
|
113
|
+
const optionalSiblingEntries = ["worker", "bootstrap"]
|
|
114
|
+
.map((baseName) => discoverSiblingEntry(dirname(serverEntry), baseName))
|
|
115
|
+
.filter((path) => path !== undefined);
|
|
116
116
|
const entrypoints = [serverEntry, ...(kumikoCli ? [kumikoCli] : []), ...optionalSiblingEntries];
|
|
117
117
|
const result = await Bun.build({
|
|
118
118
|
entrypoints,
|
|
@@ -162,12 +162,15 @@ export async function buildServerBundle(options = {}) {
|
|
|
162
162
|
return { outDir, entries, chunks, totalBytes, runtimeDeps };
|
|
163
163
|
}
|
|
164
164
|
export function discoverServerEntry(cwd) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
165
|
+
return discoverSiblingEntry(join(cwd, "bin"), "main");
|
|
166
|
+
}
|
|
167
|
+
// `.ts` wins over `.js`, same precedence for the server entry and its siblings.
|
|
168
|
+
function discoverSiblingEntry(dir, baseName) {
|
|
169
|
+
for (const extension of [".ts", ".js"]) {
|
|
170
|
+
const candidate = join(dir, `${baseName}${extension}`);
|
|
171
|
+
if (existsSync(candidate))
|
|
172
|
+
return candidate;
|
|
173
|
+
}
|
|
171
174
|
return undefined;
|
|
172
175
|
}
|
|
173
176
|
// Walking up vom App-cwd bis ein Verzeichnis mit `bin/kumiko.ts` gefunden wird.
|
package/dist/cli.d.ts
CHANGED
|
@@ -2,4 +2,3 @@ export { buildProdBundle, buildServerBundle, discoverServerEntry, formatBuildRes
|
|
|
2
2
|
export { formatScanWarning, runCodegen } from "./codegen/index.js";
|
|
3
3
|
export { createCrashTracker } from "./crash-tracker.js";
|
|
4
4
|
export { runInitDeployCli } from "./init-deploy-cli.js";
|
|
5
|
-
export { implicitAuthModeFeatureNames, resolveGeneratePath } from "./schema-check-core.js";
|
package/dist/cli.js
CHANGED
|
@@ -6,4 +6,3 @@ export { buildProdBundle, buildServerBundle, discoverServerEntry, formatBuildRes
|
|
|
6
6
|
export { formatScanWarning, runCodegen } from "./codegen/index.js";
|
|
7
7
|
export { createCrashTracker } from "./crash-tracker.js";
|
|
8
8
|
export { runInitDeployCli } from "./init-deploy-cli.js";
|
|
9
|
-
export { implicitAuthModeFeatureNames, resolveGeneratePath } from "./schema-check-core.js";
|
package/dist/compose-stacks.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type AuthMfaFeatureOptions } from "@cosmicdrift/kumiko-bundled-features
|
|
|
2
2
|
import { type LegalPagesOptions } from "@cosmicdrift/kumiko-bundled-features/legal-pages";
|
|
3
3
|
import { type UserDataRightsOptions } from "@cosmicdrift/kumiko-bundled-features/user-data-rights";
|
|
4
4
|
import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
|
+
import { type MailBrandSuffixResolver, type MailFooterResolver } from "@cosmicdrift/kumiko-renderer-mail-html";
|
|
5
6
|
export type FileProviderKind = "inmemory" | "s3" | "s3-env";
|
|
6
7
|
export type MailTransportKind = "inmemory" | "smtp";
|
|
7
8
|
export type GdprStackOrder = "retention-first" | "compliance-first";
|
|
@@ -48,7 +49,13 @@ export type IdentityStackOptions = {
|
|
|
48
49
|
};
|
|
49
50
|
export declare function stackFeatureNames(features: readonly FeatureDefinition[]): string[];
|
|
50
51
|
export declare function composePagesStack(options?: PagesStackOptions): FeatureDefinition[];
|
|
51
|
-
export
|
|
52
|
+
export type RendererStackOptions = {
|
|
53
|
+
/** Platform imprint, privacy link and footer text under every mail; see createRendererMailHtmlFeature. */
|
|
54
|
+
readonly mailFooter?: MailFooterResolver;
|
|
55
|
+
/** Word after the tenant name in the mail's brand row, e.g. "Status". */
|
|
56
|
+
readonly mailBrandSuffix?: MailBrandSuffixResolver;
|
|
57
|
+
};
|
|
58
|
+
export declare function composeRendererStack(options?: RendererStackOptions): FeatureDefinition[];
|
|
52
59
|
export declare function composeMailStack(options: MailStackOptions): FeatureDefinition[];
|
|
53
60
|
export declare function composeFileStack(options: FileStackOptions): FeatureDefinition[];
|
|
54
61
|
/** Retention + compliance (+ optional lifecycle/sessions). UDR/files mount separately — order varies per app. */
|
package/dist/compose-stacks.js
CHANGED
|
@@ -30,7 +30,7 @@ import { createTemplateResolverFeature } from "@cosmicdrift/kumiko-bundled-featu
|
|
|
30
30
|
import { createTenantLifecycleFeature } from "@cosmicdrift/kumiko-bundled-features/tenant-lifecycle";
|
|
31
31
|
import { createUserDataRightsFeature, } from "@cosmicdrift/kumiko-bundled-features/user-data-rights";
|
|
32
32
|
import { createUserDataRightsDefaultsFeature } from "@cosmicdrift/kumiko-bundled-features/user-data-rights-defaults";
|
|
33
|
-
import { createRendererMailHtmlFeature } from "@cosmicdrift/kumiko-renderer-mail-html";
|
|
33
|
+
import { createRendererMailHtmlFeature, } from "@cosmicdrift/kumiko-renderer-mail-html";
|
|
34
34
|
export function stackFeatureNames(features) {
|
|
35
35
|
return features.map((f) => f.name);
|
|
36
36
|
}
|
|
@@ -44,11 +44,14 @@ export function composePagesStack(options = {}) {
|
|
|
44
44
|
createLegalPagesFeature(options.wrapLayout !== undefined ? { wrapLayout: options.wrapLayout } : {}),
|
|
45
45
|
];
|
|
46
46
|
}
|
|
47
|
-
export function composeRendererStack() {
|
|
47
|
+
export function composeRendererStack(options = {}) {
|
|
48
48
|
return [
|
|
49
49
|
createTemplateResolverFeature(),
|
|
50
50
|
createRendererFoundationFeature(),
|
|
51
|
-
createRendererMailHtmlFeature(
|
|
51
|
+
createRendererMailHtmlFeature({
|
|
52
|
+
...(options.mailFooter !== undefined && { footer: options.mailFooter }),
|
|
53
|
+
...(options.mailBrandSuffix !== undefined && { brandSuffix: options.mailBrandSuffix }),
|
|
54
|
+
}),
|
|
52
55
|
];
|
|
53
56
|
}
|
|
54
57
|
export function composeMailStack(options) {
|
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { runInitDeployCli } from "./init-deploy-cli.js";
|
|
|
11
11
|
export type { RunDevAppAuthOptions, RunDevAppOptions, SeedFn } from "./run-dev-app.js";
|
|
12
12
|
export { runDevApp } from "./run-dev-app.js";
|
|
13
13
|
export type { ScaffoldAppOptions, ScaffoldAppResult, ScaffoldFeatureEntry, } from "./scaffold-app.js";
|
|
14
|
-
export { scaffoldApp } from "./scaffold-app.js";
|
|
14
|
+
export { SCAFFOLD_BIOME_VERSION, scaffoldApp } from "./scaffold-app.js";
|
|
15
15
|
export type { ScaffoldAppFeatureOptions, ScaffoldAppFeatureResult, } from "./scaffold-app-feature.js";
|
|
16
16
|
export { runConfigPathForApp, scaffoldAppFeature } from "./scaffold-app-feature.js";
|
|
17
17
|
export type { CheckDeployDriftResult, DeployDriftEntry, RenderDeployFilesOptions, RenderDeployFilesResult, RenderedDeployFile, ScaffoldDeployDetected, ScaffoldDeployOptions, ScaffoldDeployResult, ScaffoldedFile, } from "./scaffold-deploy.js";
|
package/dist/index.js
CHANGED
|
@@ -25,7 +25,7 @@ export { buildFewShotCorpus, pathToId } from "./few-shot-corpus.js";
|
|
|
25
25
|
export { resolveFrameworkVersion } from "./framework-version.js";
|
|
26
26
|
export { runInitDeployCli } from "./init-deploy-cli.js";
|
|
27
27
|
export { runDevApp } from "./run-dev-app.js";
|
|
28
|
-
export { scaffoldApp } from "./scaffold-app.js";
|
|
28
|
+
export { SCAFFOLD_BIOME_VERSION, scaffoldApp } from "./scaffold-app.js";
|
|
29
29
|
export { runConfigPathForApp, scaffoldAppFeature } from "./scaffold-app-feature.js";
|
|
30
30
|
export { checkDeployDrift, renderDeployFiles, scaffoldDeploy } from "./scaffold-deploy.js";
|
|
31
31
|
export { scaffoldFeature } from "./scaffold-feature.js";
|
|
@@ -2,6 +2,8 @@ import { type KmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
|
2
2
|
export type DevPiiKmsResolution = {
|
|
3
3
|
readonly kms: KmsAdapter | undefined;
|
|
4
4
|
readonly blindIndexKey: string | undefined;
|
|
5
|
+
/** True only when the ephemeral default minted the key, not when the app passed one. */
|
|
6
|
+
readonly generatedBlindIndexKey: boolean;
|
|
5
7
|
readonly mode: "app-kms" | "ephemeral-default" | "plaintext";
|
|
6
8
|
};
|
|
7
9
|
type DevPiiKmsOptions = {
|
|
@@ -8,7 +8,9 @@ export function captureEphemeralDevPiiKms(resolution) {
|
|
|
8
8
|
if (resolution.mode !== "ephemeral-default")
|
|
9
9
|
return () => { };
|
|
10
10
|
const ownKms = configuredPiiSubjectKms();
|
|
11
|
-
const ownBlindIndexKey =
|
|
11
|
+
const ownBlindIndexKey = resolution.generatedBlindIndexKey
|
|
12
|
+
? configuredBlindIndexKey()
|
|
13
|
+
: undefined;
|
|
12
14
|
return () => {
|
|
13
15
|
if (ownKms !== undefined && configuredPiiSubjectKms() === ownKms) {
|
|
14
16
|
configurePiiSubjectKms(undefined);
|
|
@@ -20,14 +22,25 @@ export function captureEphemeralDevPiiKms(resolution) {
|
|
|
20
22
|
}
|
|
21
23
|
export function resolveDevPiiKms(options, env = process.env) {
|
|
22
24
|
if (options.kms !== undefined && options.kms !== false) {
|
|
23
|
-
return {
|
|
25
|
+
return {
|
|
26
|
+
kms: options.kms,
|
|
27
|
+
blindIndexKey: options.blindIndexKey,
|
|
28
|
+
generatedBlindIndexKey: false,
|
|
29
|
+
mode: "app-kms",
|
|
30
|
+
};
|
|
24
31
|
}
|
|
25
32
|
if (options.kms === false || isPersistentDevDb(env)) {
|
|
26
|
-
return {
|
|
33
|
+
return {
|
|
34
|
+
kms: undefined,
|
|
35
|
+
blindIndexKey: options.blindIndexKey,
|
|
36
|
+
generatedBlindIndexKey: false,
|
|
37
|
+
mode: "plaintext",
|
|
38
|
+
};
|
|
27
39
|
}
|
|
28
40
|
return {
|
|
29
41
|
kms: new InMemoryKmsAdapter(),
|
|
30
42
|
blindIndexKey: options.blindIndexKey ?? randomBytes(32).toString("base64"),
|
|
43
|
+
generatedBlindIndexKey: options.blindIndexKey === undefined,
|
|
31
44
|
mode: "ephemeral-default",
|
|
32
45
|
};
|
|
33
46
|
}
|
package/dist/run-dev-app.js
CHANGED
|
@@ -22,7 +22,7 @@ import { TenantQueries } from "@cosmicdrift/kumiko-bundled-features/tenant";
|
|
|
22
22
|
import { createInMemoryLoginRateLimiter, parseTrustedProxyHopsEnv, TRUSTED_PROXY_HOPS_ENV, } from "@cosmicdrift/kumiko-framework/api";
|
|
23
23
|
import { configureBlindIndexKey, configurePiiSubjectKms, } from "@cosmicdrift/kumiko-framework/crypto";
|
|
24
24
|
import { configureEntityFieldEncryption } from "@cosmicdrift/kumiko-framework/db";
|
|
25
|
-
import { collectWriteHandlerQns, createRegistry, FILE_STORAGE_PROVIDER_BOOT_SENTINEL, FILE_STORAGE_PROVIDER_ENV, findTierResolverUsage,
|
|
25
|
+
import { collectWriteHandlerQns, createRegistry, FILE_STORAGE_PROVIDER_BOOT_SENTINEL, FILE_STORAGE_PROVIDER_ENV, findTierResolverUsage, requireTierResolverPlugin, validateAppCustomScreenWriteQns, validateBoot, } from "@cosmicdrift/kumiko-framework/engine";
|
|
26
26
|
import { warnIfNonUtcServerTimeZone } from "@cosmicdrift/kumiko-framework/time";
|
|
27
27
|
import { applyBootSeeds } from "@cosmicdrift/kumiko-server-runtime/boot/apply-boot-seeds";
|
|
28
28
|
import { resolveBootCrypto } from "@cosmicdrift/kumiko-server-runtime/boot/boot-crypto";
|
|
@@ -202,6 +202,13 @@ export async function runDevApp(options) {
|
|
|
202
202
|
let tokenVerifier;
|
|
203
203
|
const patFeature = features.find((f) => f.name === PAT_FEATURE);
|
|
204
204
|
const mfaFeature = features.find((f) => f.name === AUTH_MFA_FEATURE);
|
|
205
|
+
if (effectiveAuth &&
|
|
206
|
+
mfaFeature &&
|
|
207
|
+
registry?.getWriteHandler(AuthHandlers.switchTenantMfaGate) === undefined) {
|
|
208
|
+
throw new Error(`[runDevApp] auth-mfa is mounted but ${AuthHandlers.switchTenantMfaGate} is not registered: ` +
|
|
209
|
+
"createAuthEmailPasswordFeature registers it only when mfaStatusChecker is set, and without it " +
|
|
210
|
+
"POST /auth/switch-tenant would mint sessions without the second factor");
|
|
211
|
+
}
|
|
205
212
|
const patAuthFragment = patFeature
|
|
206
213
|
? {
|
|
207
214
|
tokenVerifier: (rawToken) => {
|
|
@@ -327,11 +334,7 @@ export async function runDevApp(options) {
|
|
|
327
334
|
// on the resolver being live (e.g. seed that writes a SystemAdmin's
|
|
328
335
|
// tier-assignment can immediately read tier-cuts).
|
|
329
336
|
if (tierResolverUsage) {
|
|
330
|
-
|
|
331
|
-
if (!isTierResolverPlugin(plugin)) {
|
|
332
|
-
throw new Error("tier-resolver registration options must provide a build() factory");
|
|
333
|
-
}
|
|
334
|
-
tierResolverHolder.resolver = await plugin.build({
|
|
337
|
+
tierResolverHolder.resolver = await requireTierResolverPlugin(tierResolverUsage).build({
|
|
335
338
|
db: stack.db,
|
|
336
339
|
registry: stack.registry,
|
|
337
340
|
cacheSync: stack.cacheSync,
|
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
// Auto-mount via ts-morph: opens src/run-config.ts, finds
|
|
9
9
|
// `export const APP_FEATURES = [...]`, prepends import + appends entry.
|
|
10
10
|
// User's promise "defineFeature → nichts woanders eintragen" is met
|
|
11
|
-
// for the run-config side
|
|
12
|
-
// touched here — DX-4 auto-discovery resolves that.
|
|
11
|
+
// for the run-config side; the schema is derived from the mounted features.
|
|
13
12
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
14
13
|
import { join, resolve } from "node:path";
|
|
15
14
|
import { Project, SyntaxKind } from "ts-morph";
|
package/dist/scaffold-app.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export type ScaffoldTestSetupFactory = (input: {
|
|
|
16
16
|
readonly appName: string;
|
|
17
17
|
readonly frameworkVersion: string;
|
|
18
18
|
}) => ScaffoldTestSetup;
|
|
19
|
+
export declare const SCAFFOLD_BIOME_VERSION = "2.5.7";
|
|
19
20
|
export type ScaffoldAppOptions = {
|
|
20
21
|
/** kebab-case app name (e.g. "my-shop"). Becomes package-name + folder. */
|
|
21
22
|
readonly name: string;
|
package/dist/scaffold-app.js
CHANGED
|
@@ -6,19 +6,28 @@
|
|
|
6
6
|
// stub, package.json with @cosmicdrift/* deps, tsconfig, .env.example,
|
|
7
7
|
// README.
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// generate + later modify — no template-string ↔ ts-morph divergence.
|
|
9
|
+
// Generated files are emitted already formatted to Biome's output (import
|
|
10
|
+
// order, 100-col wrapping, trailing commas) so a fresh app passes `biome check`.
|
|
12
11
|
// Static files (package.json, tsconfig, .env, README) stay text-based.
|
|
13
12
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
14
13
|
import { dirname, join, relative, resolve } from "node:path";
|
|
15
14
|
import { collectTableMetas, generateMigration, writeSnapshotJson, } from "@cosmicdrift/kumiko-framework/db";
|
|
16
15
|
import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";
|
|
17
|
-
import { IndentationText, Project, VariableDeclarationKind } from "ts-morph";
|
|
18
16
|
import { isKebabSegment } from "./kebab.js";
|
|
19
17
|
import { renderAgentsMd, renderClaudeMd } from "./scaffold-agent-docs.js";
|
|
20
18
|
import { createDemoTasksFeature, renderDemoSeedFile, renderDemoTasksFeatureFile, renderDemoTasksI18n, renderDemoTasksIndex, renderDemoTasksWebIndex, } from "./scaffold-demo-tasks.js";
|
|
21
19
|
import { scaffoldDeploy } from "./scaffold-deploy.js";
|
|
20
|
+
// Exact pin (no caret): the generated files are formatted to this Biome's output,
|
|
21
|
+
// and a scaffold-lint test keeps it equal to the monorepo's own Biome version.
|
|
22
|
+
export const SCAFFOLD_BIOME_VERSION = "2.5.7";
|
|
23
|
+
const SCAFFOLD_GITIGNORE = `node_modules/
|
|
24
|
+
dist/
|
|
25
|
+
dist-server/
|
|
26
|
+
.env
|
|
27
|
+
.kumiko/
|
|
28
|
+
test-results/
|
|
29
|
+
playwright-report/
|
|
30
|
+
`;
|
|
22
31
|
export async function scaffoldApp(options) {
|
|
23
32
|
if (!isKebabSegment(options.name)) {
|
|
24
33
|
throw new Error(`scaffoldApp: name must be kebab-case (a-z, 0-9, -); got "${options.name}"`);
|
|
@@ -42,6 +51,8 @@ export async function scaffoldApp(options) {
|
|
|
42
51
|
files.push("kumiko.json");
|
|
43
52
|
write(join(destination, "biome.json"), renderBiomeJson());
|
|
44
53
|
files.push("biome.json");
|
|
54
|
+
write(join(destination, ".gitignore"), SCAFFOLD_GITIGNORE);
|
|
55
|
+
files.push(".gitignore");
|
|
45
56
|
if (testSetup === undefined) {
|
|
46
57
|
write(join(destination, "bunfig.toml"), renderBunfigToml());
|
|
47
58
|
files.push("bunfig.toml");
|
|
@@ -132,7 +143,7 @@ function renderPackageJson(name, version, testSetup) {
|
|
|
132
143
|
zod: "^4.4.3",
|
|
133
144
|
},
|
|
134
145
|
devDependencies: sortedByName({
|
|
135
|
-
"@biomejs/biome":
|
|
146
|
+
"@biomejs/biome": SCAFFOLD_BIOME_VERSION,
|
|
136
147
|
"@tailwindcss/cli": "^4.3.0",
|
|
137
148
|
"@types/react": "^19.2.0",
|
|
138
149
|
"@types/react-dom": "^19.2.0",
|
|
@@ -168,7 +179,7 @@ function renderTsconfig() {
|
|
|
168
179
|
}
|
|
169
180
|
function renderBiomeJson() {
|
|
170
181
|
return `${JSON.stringify({
|
|
171
|
-
$schema:
|
|
182
|
+
$schema: `https://biomejs.dev/schemas/${SCAFFOLD_BIOME_VERSION}/schema.json`,
|
|
172
183
|
vcs: {
|
|
173
184
|
enabled: true,
|
|
174
185
|
clientKind: "git",
|
|
@@ -209,7 +220,7 @@ function renderBiomeJson() {
|
|
|
209
220
|
linter: {
|
|
210
221
|
enabled: true,
|
|
211
222
|
rules: {
|
|
212
|
-
|
|
223
|
+
preset: "recommended",
|
|
213
224
|
correctness: { noUnusedVariables: "error", noUnusedImports: "error" },
|
|
214
225
|
suspicious: { noExplicitAny: "error", noDebugger: "error", noConsole: "warn" },
|
|
215
226
|
complexity: { useLiteralKeys: "off" },
|
|
@@ -254,13 +265,6 @@ pathIgnorePatterns = [
|
|
|
254
265
|
]
|
|
255
266
|
`;
|
|
256
267
|
}
|
|
257
|
-
function newTsProject() {
|
|
258
|
-
return new Project({
|
|
259
|
-
useInMemoryFileSystem: true,
|
|
260
|
-
compilerOptions: { target: 99, module: 99, strict: true },
|
|
261
|
-
manipulationSettings: { indentationText: IndentationText.TwoSpaces },
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
268
|
const FOUNDATION_FEATURES = [
|
|
265
269
|
{
|
|
266
270
|
name: "secrets",
|
|
@@ -289,9 +293,40 @@ const FOUNDATION_FEATURES = [
|
|
|
289
293
|
// jemand scaffoldApp() direkt mit allen Bundled-Feature-Entries aufruft
|
|
290
294
|
// rutschen die 4 nicht in run-config.ts.
|
|
291
295
|
const COMPOSE_AUTO_MOUNTED_NAMES = new Set(["config", "user", "tenant", "auth-email-password"]);
|
|
296
|
+
const MAX_LINE_WIDTH = 100;
|
|
297
|
+
// Mirrors Biome's import sort so generated files pass `biome check` untouched:
|
|
298
|
+
// packages alphabetically by specifier, relative paths last.
|
|
299
|
+
function compareImportKeys(a, b) {
|
|
300
|
+
const lowerA = a.replace(/^type /, "").toLowerCase();
|
|
301
|
+
const lowerB = b.replace(/^type /, "").toLowerCase();
|
|
302
|
+
if (lowerA !== lowerB)
|
|
303
|
+
return lowerA < lowerB ? -1 : 1;
|
|
304
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
305
|
+
}
|
|
306
|
+
function isRelativeSpecifier(specifier) {
|
|
307
|
+
return specifier.startsWith("./") || specifier.startsWith("../");
|
|
308
|
+
}
|
|
309
|
+
function renderImportLine({ specifier, names, typeOnly }) {
|
|
310
|
+
const sorted = [...names].sort(compareImportKeys);
|
|
311
|
+
const keyword = typeOnly === true ? "import type" : "import";
|
|
312
|
+
const oneLine = `${keyword} { ${sorted.join(", ")} } from "${specifier}";`;
|
|
313
|
+
if (sorted.length === 1 || oneLine.length <= MAX_LINE_WIDTH)
|
|
314
|
+
return oneLine;
|
|
315
|
+
return `${keyword} {\n${sorted.map((n) => ` ${n},`).join("\n")}\n} from "${specifier}";`;
|
|
316
|
+
}
|
|
317
|
+
function renderSortedImports(imports) {
|
|
318
|
+
return [...imports]
|
|
319
|
+
.sort((a, b) => {
|
|
320
|
+
const relativeA = isRelativeSpecifier(a.specifier);
|
|
321
|
+
const relativeB = isRelativeSpecifier(b.specifier);
|
|
322
|
+
if (relativeA !== relativeB)
|
|
323
|
+
return relativeA ? 1 : -1;
|
|
324
|
+
return compareImportKeys(a.specifier, b.specifier);
|
|
325
|
+
})
|
|
326
|
+
.map(renderImportLine)
|
|
327
|
+
.join("\n");
|
|
328
|
+
}
|
|
292
329
|
function renderRunConfig(features) {
|
|
293
|
-
const project = newTsProject();
|
|
294
|
-
const sf = project.createSourceFile("run-config.ts", "");
|
|
295
330
|
// Fallback decided BEFORE filtering: if the caller passed features (even
|
|
296
331
|
// if every one of them is an auto-mounted name), an all-filtered-out empty
|
|
297
332
|
// result is the correct outcome — falling back to FOUNDATION_FEATURES here
|
|
@@ -306,289 +341,215 @@ function renderRunConfig(features) {
|
|
|
306
341
|
existing.push(entry.exportName);
|
|
307
342
|
grouped.set(entry.importPath, existing);
|
|
308
343
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
moduleSpecifier: "./features/tasks",
|
|
314
|
-
namedImports: ["tasksFeature"],
|
|
315
|
-
});
|
|
344
|
+
const imports = [
|
|
345
|
+
...[...grouped].map(([specifier, names]) => ({ specifier, names })),
|
|
346
|
+
{ specifier: "./features/tasks", names: ["tasksFeature"] },
|
|
347
|
+
];
|
|
316
348
|
const callExprs = [...effective.map((f) => f.callExpression), "tasksFeature"];
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
349
|
+
const oneLineFeatures = `export const APP_FEATURES = [${callExprs.join(", ")}] as const;`;
|
|
350
|
+
const appFeatures = oneLineFeatures.length <= MAX_LINE_WIDTH
|
|
351
|
+
? oneLineFeatures
|
|
352
|
+
: `export const APP_FEATURES = [\n${callExprs.map((c) => ` ${c},`).join("\n")}\n] as const;`;
|
|
353
|
+
return `// Single source of truth for your app's feature composition.
|
|
354
|
+
// config/user/tenant/auth-email-password are added automatically
|
|
355
|
+
// via composeFeatures(includeBundled:true) when runProdApp is called
|
|
356
|
+
// with \`auth: {…}\` (see bin/main.ts).
|
|
357
|
+
//
|
|
358
|
+
// Add new features:
|
|
359
|
+
// - bunx @cosmicdrift/kumiko-cli add feature <name> (DX-2, automatic)
|
|
360
|
+
// - or: hand-edit + add the import below
|
|
361
|
+
|
|
362
|
+
${renderSortedImports(imports)}
|
|
363
|
+
|
|
364
|
+
${appFeatures}
|
|
365
|
+
|
|
366
|
+
export const HAS_AUTH = true;
|
|
367
|
+
`;
|
|
368
|
+
}
|
|
369
|
+
function renderAdminAuthBlock(appName, extraAdminLines) {
|
|
370
|
+
const passwordAndExtras = extraAdminLines.map((l) => ` ${l}`).join("\n");
|
|
371
|
+
return ` auth: {
|
|
372
|
+
admin: {
|
|
373
|
+
email: "admin@${appName}.local",
|
|
374
|
+
${passwordAndExtras}
|
|
375
|
+
memberships: [
|
|
376
|
+
{
|
|
377
|
+
tenantId: DEFAULT_TENANT_ID,
|
|
378
|
+
tenantKey: "${appName}",
|
|
379
|
+
tenantName: "${appName}",
|
|
380
|
+
roles: ["TenantAdmin"],
|
|
381
|
+
},
|
|
382
|
+
],
|
|
383
|
+
},
|
|
384
|
+
},`;
|
|
345
385
|
}
|
|
346
386
|
function renderMain(appName) {
|
|
347
387
|
const tenantId = deriveTenantId(appName);
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
{
|
|
416
|
-
name: "kmsOpts",
|
|
417
|
-
initializer: (writer) => {
|
|
418
|
-
writer.write("{").newLine();
|
|
419
|
-
writer.writeLine(` logPrefix: "[${appName}]",`);
|
|
420
|
-
writer.writeLine(` plaintextReason: "no PLATFORM_KEK / SUBJECT_KEYS_DATABASE_URL / KUMIKO_BLIND_INDEX_KEY set — see .env.example",`);
|
|
421
|
-
writer.write("} as const");
|
|
422
|
-
},
|
|
423
|
-
},
|
|
424
|
-
],
|
|
425
|
-
});
|
|
426
|
-
sf.addVariableStatement({
|
|
427
|
-
declarationKind: VariableDeclarationKind.Const,
|
|
428
|
-
declarations: [
|
|
429
|
-
{
|
|
430
|
-
name: "kmsWiring",
|
|
431
|
-
initializer: (writer) => {
|
|
432
|
-
writer.write(`process.env["NODE_ENV"] === "production"`);
|
|
433
|
-
writer.newLine();
|
|
434
|
-
writer.write(" ? requireKmsWiring(process.env, kmsOpts)");
|
|
435
|
-
writer.newLine();
|
|
436
|
-
writer.write(" : resolveKmsWiring(process.env, kmsOpts)");
|
|
437
|
-
},
|
|
438
|
-
},
|
|
439
|
-
],
|
|
440
|
-
});
|
|
441
|
-
sf.addStatements((writer) => {
|
|
442
|
-
writer
|
|
443
|
-
.write("await runProdApp(")
|
|
444
|
-
.inlineBlock(() => {
|
|
445
|
-
writer.writeLine("features: APP_FEATURES,");
|
|
446
|
-
writer.writeLine("envSchema,");
|
|
447
|
-
writer.writeLine('staticDir: "./dist",');
|
|
448
|
-
writer.writeLine("...kmsWiring,");
|
|
449
|
-
writer.write("auth: ").inlineBlock(() => {
|
|
450
|
-
writer.write("admin: ").inlineBlock(() => {
|
|
451
|
-
writer.writeLine(`email: "admin@${appName}.local",`);
|
|
452
|
-
writer.writeLine(`password: "change-me-on-first-deploy",`);
|
|
453
|
-
writer.writeLine(`displayName: "Admin",`);
|
|
454
|
-
writer.write("memberships: [");
|
|
455
|
-
writer.indent(() => {
|
|
456
|
-
writer.inlineBlock(() => {
|
|
457
|
-
writer.writeLine("tenantId: DEFAULT_TENANT_ID,");
|
|
458
|
-
writer.writeLine(`tenantKey: "${appName}",`);
|
|
459
|
-
writer.writeLine(`tenantName: "${appName}",`);
|
|
460
|
-
writer.writeLine(`roles: ["TenantAdmin"],`);
|
|
461
|
-
});
|
|
462
|
-
writer.write(",");
|
|
463
|
-
});
|
|
464
|
-
writer.write("],");
|
|
465
|
-
});
|
|
466
|
-
});
|
|
467
|
-
})
|
|
468
|
-
.write(");");
|
|
469
|
-
});
|
|
470
|
-
sf.insertText(0, [
|
|
471
|
-
"// Production-bootstrap. KUMIKO_DRY_RUN_ENV=boot exits after",
|
|
472
|
-
"// composeFeatures + validateBoot + createRegistry without DB/Redis-connect",
|
|
473
|
-
"// (see @cosmicdrift/kumiko-server-runtime runProdApp). The real dev boot",
|
|
474
|
-
"// runs via `bunx kumiko dev` (in-repo dev-tool) with a Docker stack — DX-1.0",
|
|
475
|
-
"// only covers the boot-mode path; `kumiko dev` lands in a later DX phase.",
|
|
476
|
-
"",
|
|
477
|
-
"",
|
|
478
|
-
].join("\n"));
|
|
479
|
-
return sf.getFullText();
|
|
388
|
+
const imports = renderSortedImports([
|
|
389
|
+
{ specifier: "@cosmicdrift/kumiko-dev-server/env-schema", names: ["frameworkCoreEnvSchema"] },
|
|
390
|
+
{ specifier: "@cosmicdrift/kumiko-server-runtime", names: ["composeFeatures", "runProdApp"] },
|
|
391
|
+
{ specifier: "@cosmicdrift/kumiko-framework/engine", names: ["TenantId"], typeOnly: true },
|
|
392
|
+
{ specifier: "@cosmicdrift/kumiko-framework/env", names: ["composeEnvSchema"] },
|
|
393
|
+
{
|
|
394
|
+
specifier: "@cosmicdrift/kumiko-framework/crypto",
|
|
395
|
+
names: ["requireKmsWiring", "resolveKmsWiring"],
|
|
396
|
+
},
|
|
397
|
+
{ specifier: "zod", names: ["z"] },
|
|
398
|
+
{ specifier: "../src/run-config", names: ["APP_FEATURES", "HAS_AUTH"] },
|
|
399
|
+
]);
|
|
400
|
+
const authBlock = renderAdminAuthBlock(appName, [
|
|
401
|
+
"// runProdApp validates envSchema before seeding: a missing or short value aborts the boot.",
|
|
402
|
+
'password: process.env["DEMO_ADMIN_PASSWORD"] ?? "",',
|
|
403
|
+
'displayName: "Admin",',
|
|
404
|
+
]);
|
|
405
|
+
// The envSchema must cover the SAME features runProdApp mounts at boot:
|
|
406
|
+
// `auth: { admin: … }` makes runProdApp auto-mix config/user/tenant/
|
|
407
|
+
// auth-email-password, so the JWT_SECRET (min-32) declaration has to be part
|
|
408
|
+
// of the boot-gate here too. Subject-key KMS is optional locally (plaintext +
|
|
409
|
+
// loud warning) but required under NODE_ENV=production (fw#2317 / #2339).
|
|
410
|
+
return `// Production-bootstrap. KUMIKO_DRY_RUN_ENV=boot exits after
|
|
411
|
+
// composeFeatures + validateBoot + createRegistry without DB/Redis-connect
|
|
412
|
+
// (see @cosmicdrift/kumiko-server-runtime runProdApp). The real dev boot
|
|
413
|
+
// runs via \`bunx kumiko dev\` (in-repo dev-tool) with a Docker stack — DX-1.0
|
|
414
|
+
// only covers the boot-mode path; \`kumiko dev\` lands in a later DX phase.
|
|
415
|
+
|
|
416
|
+
${imports}
|
|
417
|
+
|
|
418
|
+
const DEFAULT_TENANT_ID = "${tenantId}" as TenantId;
|
|
419
|
+
|
|
420
|
+
const bootFeatures = composeFeatures(APP_FEATURES, { includeBundled: HAS_AUTH });
|
|
421
|
+
|
|
422
|
+
const envSchema = composeEnvSchema({
|
|
423
|
+
core: frameworkCoreEnvSchema,
|
|
424
|
+
features: bootFeatures,
|
|
425
|
+
extend: z.object({
|
|
426
|
+
DEMO_ADMIN_PASSWORD: z
|
|
427
|
+
.string()
|
|
428
|
+
.min(12)
|
|
429
|
+
.describe(
|
|
430
|
+
"Password of the admin seeded on first boot (admin@${appName}.local). Generate with: openssl rand -hex 24",
|
|
431
|
+
)
|
|
432
|
+
.meta({ kumiko: { pulumi: { secret: true } } }),
|
|
433
|
+
}),
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const kmsOpts = {
|
|
437
|
+
logPrefix: "[${appName}]",
|
|
438
|
+
plaintextReason:
|
|
439
|
+
"no PLATFORM_KEK / SUBJECT_KEYS_DATABASE_URL / KUMIKO_BLIND_INDEX_KEY set — see .env.example",
|
|
440
|
+
} as const;
|
|
441
|
+
|
|
442
|
+
const kmsWiring =
|
|
443
|
+
process.env["NODE_ENV"] === "production"
|
|
444
|
+
? requireKmsWiring(process.env, kmsOpts)
|
|
445
|
+
: resolveKmsWiring(process.env, kmsOpts);
|
|
446
|
+
|
|
447
|
+
await runProdApp({
|
|
448
|
+
features: APP_FEATURES,
|
|
449
|
+
envSchema,
|
|
450
|
+
staticDir: "./dist",
|
|
451
|
+
...kmsWiring,
|
|
452
|
+
${authBlock}
|
|
453
|
+
});
|
|
454
|
+
`;
|
|
480
455
|
}
|
|
481
456
|
function renderDev(appName) {
|
|
482
457
|
const tenantId = deriveTenantId(appName);
|
|
483
|
-
const
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
});
|
|
536
|
-
writer.write(",");
|
|
537
|
-
});
|
|
538
|
-
writer.write("],");
|
|
539
|
-
});
|
|
540
|
-
});
|
|
541
|
-
})
|
|
542
|
-
.write(");");
|
|
543
|
-
});
|
|
544
|
-
sf.insertText(0, [
|
|
545
|
-
"// Dev bootstrap. `bun --watch bin/dev.ts` (see package.json scripts.dev)",
|
|
546
|
-
"// starts a full-featured dev server with auto-reload on code changes.",
|
|
547
|
-
"// setupTestStack creates missing entity tables automatically — a new",
|
|
548
|
-
"// r.entity(...) in a feature becomes a CREATE TABLE on the next reboot,",
|
|
549
|
-
"// no manual `kumiko schema apply` needed (that's prod-only).",
|
|
550
|
-
"// Persistent DB via KUMIKO_DEV_DB_NAME (.env) so admin + data survive reboots.",
|
|
551
|
-
"",
|
|
552
|
-
"",
|
|
553
|
-
].join("\n"));
|
|
554
|
-
return sf.getFullText();
|
|
458
|
+
const imports = renderSortedImports([
|
|
459
|
+
{ specifier: "@cosmicdrift/kumiko-dev-server", names: ["runDevApp"] },
|
|
460
|
+
{ specifier: "@cosmicdrift/kumiko-framework/engine", names: ["TenantId"], typeOnly: true },
|
|
461
|
+
{ specifier: "../src/run-config", names: ["APP_FEATURES"] },
|
|
462
|
+
{ specifier: "../src/seed", names: ["seedDemoTasks"] },
|
|
463
|
+
]);
|
|
464
|
+
// Dev-only emailVerified skips the login verify gate for the seeded admin;
|
|
465
|
+
// prod's bin/main.ts leaves it unset because the verify flow must run there.
|
|
466
|
+
const authBlock = renderAdminAuthBlock(appName, [
|
|
467
|
+
'password: "changeme",',
|
|
468
|
+
'displayName: "Admin",',
|
|
469
|
+
"emailVerified: true,",
|
|
470
|
+
]);
|
|
471
|
+
return `// Dev bootstrap. \`bun --watch bin/dev.ts\` (see package.json scripts.dev)
|
|
472
|
+
// starts a full-featured dev server with auto-reload on code changes.
|
|
473
|
+
// setupTestStack creates missing entity tables automatically — a new
|
|
474
|
+
// r.entity(...) in a feature becomes a CREATE TABLE on the next reboot,
|
|
475
|
+
// no manual \`kumiko schema apply\` needed (that's prod-only).
|
|
476
|
+
// Persistent DB via KUMIKO_DEV_DB_NAME (.env) so admin + data survive reboots.
|
|
477
|
+
|
|
478
|
+
${imports}
|
|
479
|
+
|
|
480
|
+
const DEFAULT_TENANT_ID = "${tenantId}" as TenantId;
|
|
481
|
+
|
|
482
|
+
await runDevApp({
|
|
483
|
+
features: APP_FEATURES,
|
|
484
|
+
welcomeBanner: true,
|
|
485
|
+
clientEntry: "./src/client.tsx",
|
|
486
|
+
seeds: [seedDemoTasks],
|
|
487
|
+
${authBlock}
|
|
488
|
+
});
|
|
489
|
+
`;
|
|
490
|
+
}
|
|
491
|
+
// Tiers mirror how Biome wraps the JSX as the name grows.
|
|
492
|
+
function renderDefaultAppShellElement(appName) {
|
|
493
|
+
const spanOpen = '<span className="font-semibold tracking-tight">';
|
|
494
|
+
// html-ok: appName is kebab-validated (a-z/0-9/-); this is generated .tsx source, not runtime HTML.
|
|
495
|
+
const brandInline = `${spanOpen}${appName}</span>`;
|
|
496
|
+
const singleLine = ` <DefaultAppShell brand={${brandInline}} schema={schema}>`;
|
|
497
|
+
if (singleLine.length <= MAX_LINE_WIDTH)
|
|
498
|
+
return singleLine;
|
|
499
|
+
const brandAttrInline = ` brand={${brandInline}}`;
|
|
500
|
+
const brandAttribute = brandAttrInline.length <= MAX_LINE_WIDTH
|
|
501
|
+
? brandAttrInline
|
|
502
|
+
: `${[
|
|
503
|
+
" brand={",
|
|
504
|
+
...(` ${brandInline}`.length <= MAX_LINE_WIDTH
|
|
505
|
+
? [` ${brandInline}`]
|
|
506
|
+
: [` ${spanOpen}`, ` ${appName}`, " </span>"]),
|
|
507
|
+
" }",
|
|
508
|
+
].join("\n")}`;
|
|
509
|
+
return [" <DefaultAppShell", brandAttribute, " schema={schema}", " >"].join("\n");
|
|
555
510
|
}
|
|
556
511
|
function renderClient(appName) {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
"
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
512
|
+
const imports = renderSortedImports([
|
|
513
|
+
{
|
|
514
|
+
specifier: "@cosmicdrift/kumiko-bundled-features/auth-email-password/web",
|
|
515
|
+
names: ["emailPasswordClient"],
|
|
516
|
+
},
|
|
517
|
+
{ specifier: "./features/tasks/web", names: ["tasksClient"] },
|
|
518
|
+
{
|
|
519
|
+
specifier: "@cosmicdrift/kumiko-renderer-web",
|
|
520
|
+
names: ["type AppSchema", "createKumikoApp", "DefaultAppShell"],
|
|
521
|
+
},
|
|
522
|
+
{ specifier: "react", names: ["ReactNode"], typeOnly: true },
|
|
523
|
+
]);
|
|
524
|
+
return `// Browser entry. runDevApp's clientEntry option bundles this file to
|
|
525
|
+
// /client.js and the default HTML loads it. createKumikoApp fetches the
|
|
526
|
+
// AppSchema itself from the authenticated GET /api/schema and mounts
|
|
527
|
+
// the routes.
|
|
528
|
+
//
|
|
529
|
+
// AppShell wraps DefaultAppShell to supply \`brand\` — createKumikoApp's
|
|
530
|
+
// shell option only injects schema + children. Without \`shell\` the
|
|
531
|
+
// active screen renders with no layout wrapper (a bare banner after
|
|
532
|
+
// login instead of the app). emailPasswordClient() brings the login
|
|
533
|
+
// screen + session provider — without it /login stays empty.
|
|
534
|
+
//
|
|
535
|
+
// Add new client plugins (e.g. notificationsClient()) to clientFeatures
|
|
536
|
+
// here — symmetric to APP_FEATURES on the server side.
|
|
537
|
+
|
|
538
|
+
${imports}
|
|
539
|
+
|
|
540
|
+
function AppShell({ children, schema }: { children: ReactNode; schema: AppSchema }): ReactNode {
|
|
541
|
+
return (
|
|
542
|
+
${renderDefaultAppShellElement(appName)}
|
|
543
|
+
{children}
|
|
544
|
+
</DefaultAppShell>
|
|
545
|
+
);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
createKumikoApp({
|
|
549
|
+
shell: AppShell,
|
|
550
|
+
clientFeatures: [emailPasswordClient(), tasksClient],
|
|
551
|
+
});
|
|
552
|
+
`;
|
|
592
553
|
}
|
|
593
554
|
function renderEnvExample(appName) {
|
|
594
555
|
const devDb = `${appName.replace(/-/g, "_")}_dev`;
|
|
@@ -607,6 +568,11 @@ JWT_SECRET=change-me-min-32-chars-change-me-min-32
|
|
|
607
568
|
# Generate with: openssl rand -base64 32
|
|
608
569
|
KUMIKO_SECRETS_MASTER_KEY_V1=
|
|
609
570
|
|
|
571
|
+
# DEMO_ADMIN_PASSWORD: password of the admin seeded on first prod boot
|
|
572
|
+
# (admin@${appName}.local). Required by \`bun run boot\` and \`bun run start\`,
|
|
573
|
+
# min 12 chars. Generate with: openssl rand -hex 24
|
|
574
|
+
DEMO_ADMIN_PASSWORD=
|
|
575
|
+
|
|
610
576
|
# Subject-keys KMS for PII fields (user, tenant-invitation, fileRef). All-or-none.
|
|
611
577
|
# Unset → local boot with plaintext PII + warning; NODE_ENV=production requires
|
|
612
578
|
# the trio (requireKmsWiring). Generate PLATFORM_KEK / KUMIKO_BLIND_INDEX_KEY
|
|
@@ -641,6 +607,7 @@ services:
|
|
|
641
607
|
environment:
|
|
642
608
|
POSTGRES_USER: postgres
|
|
643
609
|
POSTGRES_PASSWORD: postgres
|
|
610
|
+
POSTGRES_DB: app
|
|
644
611
|
ports:
|
|
645
612
|
- "127.0.0.1:5432:5432"
|
|
646
613
|
volumes:
|
|
@@ -691,7 +658,8 @@ bun run boot
|
|
|
691
658
|
\`\`\`
|
|
692
659
|
|
|
693
660
|
Runs \`KUMIKO_DRY_RUN_ENV=boot bun bin/main.ts\` — validates feature composition
|
|
694
|
-
+ env schema, exits 0 without touching DB/Redis. Useful in CI.
|
|
661
|
+
+ env schema, exits 0 without touching DB/Redis. Useful in CI. It needs
|
|
662
|
+
\`DEMO_ADMIN_PASSWORD\` (min 12 chars, \`openssl rand -hex 24\`) in \`.env\`.
|
|
695
663
|
|
|
696
664
|
Without \`PLATFORM_KEK\` / \`SUBJECT_KEYS_DATABASE_URL\` / \`KUMIKO_BLIND_INDEX_KEY\`
|
|
697
665
|
set (see \`.env.example\`), both \`bun run boot\` and \`bun run start\` boot with
|
|
@@ -706,6 +674,9 @@ bun run schema:apply # apply checked-in kumiko/migrations (needs DATABASE_URL)
|
|
|
706
674
|
bun run start # runProdApp against dist/
|
|
707
675
|
\`\`\`
|
|
708
676
|
|
|
677
|
+
The first prod boot seeds \`admin@${appName}.local\` with the password from
|
|
678
|
+
\`DEMO_ADMIN_PASSWORD\` (required, min 12 chars). Log in with that.
|
|
679
|
+
|
|
709
680
|
After adding entities/features, regenerate migrations:
|
|
710
681
|
|
|
711
682
|
\`\`\`sh
|
|
@@ -746,9 +717,9 @@ function renderKumikoSchema() {
|
|
|
746
717
|
"// Computes table-metas from the SAME composeFeatures(APP_FEATURES) the",
|
|
747
718
|
"// runtime sees (runProdApp/runDevApp) — migration and runtime cannot drift.",
|
|
748
719
|
"",
|
|
749
|
-
'import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";',
|
|
750
720
|
'import { collectTableMetas, type EntityTableMeta } from "@cosmicdrift/kumiko-framework/db";',
|
|
751
721
|
'import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";',
|
|
722
|
+
'import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";',
|
|
752
723
|
'import { APP_FEATURES, HAS_AUTH } from "../src/run-config";',
|
|
753
724
|
"",
|
|
754
725
|
"export const FEATURES: readonly FeatureDefinition[] = composeFeatures([...APP_FEATURES], {",
|
|
@@ -779,10 +750,10 @@ function renderBinKumiko() {
|
|
|
779
750
|
"// ad-hoc SQL. kumiko-build bundles this file to dist-server/kumiko.js.",
|
|
780
751
|
"",
|
|
781
752
|
'import { frameworkCoreEnvSchema } from "@cosmicdrift/kumiko-dev-server/env-schema";',
|
|
782
|
-
'import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";',
|
|
783
753
|
'import { runConsumerCli } from "@cosmicdrift/kumiko-framework/consumer-cli";',
|
|
784
754
|
'import { composeEnvSchema, kmsSlotsOf } from "@cosmicdrift/kumiko-framework/env";',
|
|
785
755
|
'import { runSchemaCli } from "@cosmicdrift/kumiko-framework/schema-cli";',
|
|
756
|
+
'import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";',
|
|
786
757
|
'import { APP_FEATURES, HAS_AUTH } from "../src/run-config";',
|
|
787
758
|
"",
|
|
788
759
|
"const [, , cmd, ...rest] = Bun.argv;",
|
|
@@ -792,7 +763,9 @@ function renderBinKumiko() {
|
|
|
792
763
|
'if (cmd === "schema") {',
|
|
793
764
|
" const features = composeFeatures([...APP_FEATURES], { includeBundled: HAS_AUTH });",
|
|
794
765
|
" const kmsSlots = kmsSlotsOf(composeEnvSchema({ core: frameworkCoreEnvSchema, features }).schema);",
|
|
795
|
-
" process.exit(
|
|
766
|
+
" process.exit(",
|
|
767
|
+
" await runSchemaCli(rest, process.env.INIT_CWD ?? process.cwd(), out, { features, kmsSlots }),",
|
|
768
|
+
" );",
|
|
796
769
|
"}",
|
|
797
770
|
"",
|
|
798
771
|
'if (cmd === "consumer") {',
|
|
@@ -800,7 +773,9 @@ function renderBinKumiko() {
|
|
|
800
773
|
"}",
|
|
801
774
|
"",
|
|
802
775
|
"// biome-ignore lint/suspicious/noConsole: CLI output is the feature.",
|
|
803
|
-
|
|
776
|
+
"console.error(",
|
|
777
|
+
` \`\\n Unknown: kumiko \${cmd ?? ""} — only 'kumiko schema <sub>' or 'kumiko consumer <sub>' in the standalone bundle.\\n\`,`,
|
|
778
|
+
");",
|
|
804
779
|
"process.exit(1);",
|
|
805
780
|
"",
|
|
806
781
|
].join("\n");
|
|
@@ -127,7 +127,8 @@ const editScreen: EntityEditScreenDefinition = {
|
|
|
127
127
|
const open = {
|
|
128
128
|
access: {
|
|
129
129
|
openToAll: {
|
|
130
|
-
reason:
|
|
130
|
+
reason:
|
|
131
|
+
"scaffolded demo handler: every signed-in user of the demo tenant manages their tasks",
|
|
131
132
|
},
|
|
132
133
|
},
|
|
133
134
|
} as const;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-dev-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.365.1",
|
|
4
4
|
"description": "Dev-tooling for Kumiko apps: local dev-server bootstrap (runDevApp), scaffolding, codegen. Its compose-stacks/env-schema subpaths are consumed at prod boot too — see @cosmicdrift/kumiko-server-runtime for the prod runner itself.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -60,17 +60,16 @@
|
|
|
60
60
|
"kumiko-dev": "./bin/kumiko-dev.ts",
|
|
61
61
|
"kumiko-init-deploy": "./bin/kumiko-init-deploy.ts",
|
|
62
62
|
"kumiko-schema": "./bin/kumiko-schema.ts",
|
|
63
|
-
"kumiko-schema-check": "./bin/kumiko-schema-check.ts",
|
|
64
63
|
"kumiko-upgrade": "./bin/kumiko-upgrade.ts"
|
|
65
64
|
},
|
|
66
65
|
"dependencies": {
|
|
67
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
68
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
69
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
70
|
-
"@cosmicdrift/kumiko-renderer-mail-html": "0.
|
|
71
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
72
|
-
"@cosmicdrift/kumiko-server-runtime": "0.
|
|
73
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
66
|
+
"@cosmicdrift/kumiko-bundled-features": "0.365.1",
|
|
67
|
+
"@cosmicdrift/kumiko-framework": "0.365.1",
|
|
68
|
+
"@cosmicdrift/kumiko-headless": "0.365.1",
|
|
69
|
+
"@cosmicdrift/kumiko-renderer-mail-html": "0.365.1",
|
|
70
|
+
"@cosmicdrift/kumiko-renderer-web": "0.365.1",
|
|
71
|
+
"@cosmicdrift/kumiko-server-runtime": "0.365.1",
|
|
72
|
+
"@cosmicdrift/kumiko-types": "0.365.1",
|
|
74
73
|
"bun-types": "^1.3.13",
|
|
75
74
|
"ts-morph": "^28.0.0",
|
|
76
75
|
"zod": "^4.4.3"
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,41 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.365.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "New projects read the prod admin password from DEMO_ADMIN_PASSWORD",
|
|
6
|
+
"detail": "The scaffolded bin/main.ts no longer seeds the prod admin with a fixed password. It reads DEMO_ADMIN_PASSWORD (declared in the env schema, min 12 chars), and boot fails without it. Existing apps are untouched; apps from an older scaffold should change bin/main.ts the same way."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.365.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "A freshly scaffolded app passes bun run lint without rework",
|
|
12
|
+
"detail": "New projects now get a .gitignore, an exactly pinned @biomejs/biome and lint-clean generated files (import order, formatting). Existing apps are untouched."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.365.0",
|
|
16
|
+
"type": "improvement",
|
|
17
|
+
"title": "composeRendererStack passes a mail brand suffix resolver",
|
|
18
|
+
"detail": "composeRendererStack({ mailBrandSuffix }) hands the resolver to createRendererMailHtmlFeature as brandSuffix."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"version": "0.365.0",
|
|
22
|
+
"type": "improvement",
|
|
23
|
+
"title": "composeRendererStack passes a mail footer resolver to the mail renderer",
|
|
24
|
+
"detail": "composeRendererStack({ mailFooter }) hands the resolver to createRendererMailHtmlFeature, so apps on the composed stack set imprint, privacy and platform line in one place."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"version": "0.365.0",
|
|
28
|
+
"type": "breaking",
|
|
29
|
+
"title": "The kumiko-schema-check bin and the resolveGeneratePath, usesDerivedSchemaLayout and implicitAuthModeFeatureNames exports are removed",
|
|
30
|
+
"detail": "kumiko-schema-check diffed APP_FEATURES against a hand-kept FEATURE_IMPORT_REGISTRY. Apps with kumiko/schema.ts derive their entities from the mounted features and have no registry that could drift, so the check was a no-op there. The helpers resolveGeneratePath, usesDerivedSchemaLayout and implicitAuthModeFeatureNames are no longer exported from @cosmicdrift/kumiko-dev-server/cli.",
|
|
31
|
+
"migration": "Apps with kumiko/schema.ts need nothing. Run kumiko-schema validate to check migrations against the mounted features, and remove kumiko-schema-check from package scripts and CI. Apps that keep a FEATURE_IMPORT_REGISTRY diff it against APP_FEATURES in their own coverage script."
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"version": "0.365.0",
|
|
35
|
+
"type": "fix",
|
|
36
|
+
"title": "buildServerBundle resolves worker and bootstrap entries as .ts or .js; runDevApp stop keeps an explicit blindIndexKey",
|
|
37
|
+
"detail": "An app with bin/main.js and bin/worker.js used to get no dist-server/worker.js without any warning, because only worker.ts and bootstrap.ts were looked up. Siblings now resolve like the server entry, .ts first and then .js. In the ephemeral PII default, stopping runDevApp unset a blindIndexKey that the app passed itself; it now unsets only the key the default generated."
|
|
38
|
+
},
|
|
2
39
|
{
|
|
3
40
|
"version": "0.364.0",
|
|
4
41
|
"type": "fix",
|
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
// biome-ignore-all lint/suspicious/noConsole: CLI-Script, console ist Feature.
|
|
3
|
-
//
|
|
4
|
-
// kumiko schema check — Empfehlung 3 aus Sprint-9.8-Retro
|
|
5
|
-
// (luminous-watching-moler.md). Diff't APP_FEATURES (runtime, aus
|
|
6
|
-
// `src/run-config.ts`) gegen FEATURE_IMPORT_REGISTRY (statisch, aus
|
|
7
|
-
// `drizzle/generate.ts`). Catches:
|
|
8
|
-
//
|
|
9
|
-
// 1. Mount-without-registry: ein neues feature in APP_FEATURES ohne
|
|
10
|
-
// Entry in FEATURE_IMPORT_REGISTRY. Resultiert in Schema-Drift:
|
|
11
|
-
// Runtime mountet feature, Migration kennt seine Tabellen nicht.
|
|
12
|
-
// 2. Stale-registry: ein Entry in FEATURE_IMPORT_REGISTRY ohne
|
|
13
|
-
// matching mount in APP_FEATURES. Dead-code; im Schema entsteht
|
|
14
|
-
// eine Tabelle ohne Runtime-Konsument.
|
|
15
|
-
//
|
|
16
|
-
// Studio's 9.8-Drama: FEATURE_IMPORT_REGISTRY war 18 features hinter
|
|
17
|
-
// APP_FEATURES. Hätte mit diesem check eine Sekunde Lokal gefangen.
|
|
18
|
-
//
|
|
19
|
-
// Usage (aus dem app-workspace):
|
|
20
|
-
// bunx kumiko-schema-check
|
|
21
|
-
// # oder mit explicit pfaden:
|
|
22
|
-
// bunx kumiko-schema-check --run-config src/run-config.ts --generate drizzle/generate.ts
|
|
23
|
-
//
|
|
24
|
-
// Exit 0 wenn alles in sync, exit 1 wenn drift.
|
|
25
|
-
|
|
26
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
27
|
-
import { resolve } from "node:path";
|
|
28
|
-
import {
|
|
29
|
-
implicitAuthModeFeatureNames,
|
|
30
|
-
resolveGeneratePath,
|
|
31
|
-
} from "@cosmicdrift/kumiko-dev-server/cli";
|
|
32
|
-
|
|
33
|
-
type Args = {
|
|
34
|
-
readonly runConfigPath: string;
|
|
35
|
-
readonly generatePath: string;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
function parseArgs(argv: readonly string[]): Args {
|
|
39
|
-
const cwd = process.cwd();
|
|
40
|
-
let runConfigPath = resolve(cwd, "src/run-config.ts");
|
|
41
|
-
let generatePath: string | undefined;
|
|
42
|
-
for (let i = 0; i < argv.length; i++) {
|
|
43
|
-
const flag = argv[i];
|
|
44
|
-
const value = argv[i + 1];
|
|
45
|
-
if (flag === "--run-config" && value) {
|
|
46
|
-
runConfigPath = resolve(cwd, value);
|
|
47
|
-
i++;
|
|
48
|
-
} else if (flag === "--generate" && value) {
|
|
49
|
-
generatePath = resolve(cwd, value);
|
|
50
|
-
i++;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return { runConfigPath, generatePath: generatePath ?? resolveGeneratePath(cwd) };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function readRegistryFeatures(generateSrc: string): Set<string> {
|
|
57
|
-
// Match object-keys mit Discriminator `kind: "factory" | "named"`.
|
|
58
|
-
// Quoted ("billing-foundation") oder unquoted (config, user) — beides
|
|
59
|
-
// ist gültig in JS. Pattern aus use-all-bundled/scripts/check-coverage.ts
|
|
60
|
-
// dupliziert hier weil per-app-CLI nicht von sample-script lesen darf.
|
|
61
|
-
const re = /(?:"([a-z][a-z0-9-]*)"|([a-z][a-z0-9]*)):\s*\{\s*kind:\s*"(factory|named)"/g;
|
|
62
|
-
const out = new Set<string>();
|
|
63
|
-
for (const m of generateSrc.matchAll(re)) {
|
|
64
|
-
const name = m[1] ?? m[2];
|
|
65
|
-
if (name) out.add(name);
|
|
66
|
-
}
|
|
67
|
-
return out;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
async function readMountedFeatures(runConfigPath: string): Promise<Set<string>> {
|
|
71
|
-
const mod = (await import(runConfigPath)) as {
|
|
72
|
-
APP_FEATURES?: ReadonlyArray<{ name: string }>;
|
|
73
|
-
HAS_AUTH?: boolean;
|
|
74
|
-
HAS_SIGNUP?: boolean;
|
|
75
|
-
};
|
|
76
|
-
if (!mod.APP_FEATURES) {
|
|
77
|
-
throw new Error(
|
|
78
|
-
`kumiko-schema-check: ${runConfigPath} hat kein 'APP_FEATURES' export. ` +
|
|
79
|
-
`Convention: 'export const APP_FEATURES = [...] as const'.`,
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
const set = new Set<string>();
|
|
83
|
-
for (const f of mod.APP_FEATURES) {
|
|
84
|
-
set.add(f.name);
|
|
85
|
-
}
|
|
86
|
-
// HAS_AUTH defaults to true (Studio/use-all-bundled convention). When set,
|
|
87
|
-
// include the implicit auth-mode features so the diff doesn't false-positive
|
|
88
|
-
// "auth-email-password is mounted but no registry-entry".
|
|
89
|
-
if (mod.HAS_AUTH ?? true) {
|
|
90
|
-
for (const name of implicitAuthModeFeatureNames()) set.add(name);
|
|
91
|
-
}
|
|
92
|
-
// auth-self-registration is only prepended when authOptions.signup is set
|
|
93
|
-
// (composeFeatures) — opt-in via HAS_SIGNUP so apps without signup don't
|
|
94
|
-
// get a stale-registry false positive (#1521).
|
|
95
|
-
if (mod.HAS_SIGNUP === true) {
|
|
96
|
-
set.add("auth-self-registration");
|
|
97
|
-
}
|
|
98
|
-
return set;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const USAGE = "Usage: kumiko-schema-check [--run-config <path>] [--generate <path>]";
|
|
102
|
-
|
|
103
|
-
async function main(): Promise<void> {
|
|
104
|
-
if (process.argv.slice(2).some((arg) => arg === "--help" || arg === "-h")) {
|
|
105
|
-
console.log(USAGE);
|
|
106
|
-
process.exit(0);
|
|
107
|
-
}
|
|
108
|
-
const args = parseArgs(process.argv.slice(2));
|
|
109
|
-
|
|
110
|
-
if (!existsSync(args.runConfigPath)) {
|
|
111
|
-
console.error(`✗ run-config not found: ${args.runConfigPath}`);
|
|
112
|
-
process.exit(1);
|
|
113
|
-
}
|
|
114
|
-
if (!existsSync(args.generatePath)) {
|
|
115
|
-
console.error(`✗ generate not found: ${args.generatePath}`);
|
|
116
|
-
process.exit(1);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const registry = readRegistryFeatures(readFileSync(args.generatePath, "utf-8"));
|
|
120
|
-
const mounted = await readMountedFeatures(args.runConfigPath);
|
|
121
|
-
|
|
122
|
-
// Mounted but not in registry → schema-drift (runtime ↔ migration mismatch).
|
|
123
|
-
const mountedWithoutEntry: string[] = [];
|
|
124
|
-
for (const name of mounted) {
|
|
125
|
-
if (!registry.has(name)) mountedWithoutEntry.push(name);
|
|
126
|
-
}
|
|
127
|
-
// In registry but not mounted → stale entry (dead schema-mapping).
|
|
128
|
-
const staleEntries: string[] = [];
|
|
129
|
-
for (const name of registry) {
|
|
130
|
-
if (!mounted.has(name)) staleEntries.push(name);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
let ok = true;
|
|
134
|
-
|
|
135
|
-
if (mountedWithoutEntry.length > 0) {
|
|
136
|
-
console.error(
|
|
137
|
-
`\n✗ ${mountedWithoutEntry.length} feature(s) mounted in APP_FEATURES but NOT in FEATURE_IMPORT_REGISTRY:`,
|
|
138
|
-
);
|
|
139
|
-
for (const name of mountedWithoutEntry.sort()) {
|
|
140
|
-
console.error(` - ${name}`);
|
|
141
|
-
}
|
|
142
|
-
console.error(
|
|
143
|
-
"\n Action: in drizzle/generate.ts FEATURE_IMPORT_REGISTRY den Eintrag ergänzen,",
|
|
144
|
-
);
|
|
145
|
-
console.error(" damit Schema-Generator + Migration die feature-Tabellen kennt.");
|
|
146
|
-
ok = false;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (staleEntries.length > 0) {
|
|
150
|
-
console.error(
|
|
151
|
-
`\n✗ ${staleEntries.length} stale FEATURE_IMPORT_REGISTRY entry/entries (kein matching mount):`,
|
|
152
|
-
);
|
|
153
|
-
for (const name of staleEntries.sort()) {
|
|
154
|
-
console.error(` - ${name}`);
|
|
155
|
-
}
|
|
156
|
-
console.error("\n Action: entry aus drizzle/generate.ts FEATURE_IMPORT_REGISTRY entfernen,");
|
|
157
|
-
console.error(" oder das feature in src/run-config.ts mounten.");
|
|
158
|
-
ok = false;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (ok) {
|
|
162
|
-
console.log(
|
|
163
|
-
`✓ schema check: ${mounted.size} mounted ↔ ${registry.size} registry entries, no drift`,
|
|
164
|
-
);
|
|
165
|
-
process.exit(0);
|
|
166
|
-
} else {
|
|
167
|
-
process.exit(1);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Only run when executed as a CLI, not when imported (e.g. from tests that
|
|
172
|
-
// exercise the exported pure helpers).
|
|
173
|
-
if (import.meta.main) {
|
|
174
|
-
await main();
|
|
175
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
// Pure helpers for the `kumiko-schema-check` bin. Kept in src/ (vs. the bin)
|
|
2
|
-
// so they are part of the tsc project + unit-testable without importing the
|
|
3
|
-
// auto-running CLI.
|
|
4
|
-
import { existsSync } from "node:fs";
|
|
5
|
-
import { resolve } from "node:path";
|
|
6
|
-
import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";
|
|
7
|
-
// The generate-script lives under different roots across apps: publicstatus
|
|
8
|
-
// uses `drizzle/generate.ts`, the framework's own sample-apps use
|
|
9
|
-
// `schema/generate.ts`. Pick whichever exists so a no-arg `bunx
|
|
10
|
-
// kumiko-schema-check` works in both layouts; default to `drizzle/` for the
|
|
11
|
-
// error message when neither is present.
|
|
12
|
-
export function resolveGeneratePath(cwd) {
|
|
13
|
-
const drizzlePath = resolve(cwd, "drizzle/generate.ts");
|
|
14
|
-
const schemaPath = resolve(cwd, "schema/generate.ts");
|
|
15
|
-
if (!existsSync(drizzlePath) && existsSync(schemaPath))
|
|
16
|
-
return schemaPath;
|
|
17
|
-
return drizzlePath;
|
|
18
|
-
}
|
|
19
|
-
// The features composeFeatures auto-prepends in auth-mode (config + user +
|
|
20
|
-
// tenant + auth-email-password). Derived from composeFeatures itself so this
|
|
21
|
-
// can't drift from the real prepend-list in compose-features.ts.
|
|
22
|
-
export function implicitAuthModeFeatureNames() {
|
|
23
|
-
return composeFeatures([], { includeBundled: true }).map((f) => f.name);
|
|
24
|
-
}
|