@cosmicdrift/kumiko-dev-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/bin/kumiko-build.ts +85 -0
  2. package/bin/kumiko-dev.ts +90 -0
  3. package/package.json +45 -0
  4. package/src/__tests__/build-prod-bundle.integration.ts +265 -0
  5. package/src/__tests__/build-prod-bundle.test.ts +262 -0
  6. package/src/__tests__/cache-headers.test.ts +70 -0
  7. package/src/__tests__/classify-change.test.ts +87 -0
  8. package/src/__tests__/compose-features-wiring.integration.ts +352 -0
  9. package/src/__tests__/compose-features.test.ts +81 -0
  10. package/src/__tests__/crash-tracker.test.ts +89 -0
  11. package/src/__tests__/create-kumiko-server.integration.ts +286 -0
  12. package/src/__tests__/few-shot-corpus.test.ts +311 -0
  13. package/src/__tests__/inject-schema.test.ts +62 -0
  14. package/src/__tests__/resolve-stylesheet.test.ts +90 -0
  15. package/src/__tests__/resolve-tailwind-cli.test.ts +49 -0
  16. package/src/__tests__/run-prod-app-spec.test.ts +57 -0
  17. package/src/__tests__/run-prod-app.integration.ts +535 -0
  18. package/src/__tests__/scaffold-feature.test.ts +143 -0
  19. package/src/__tests__/try-hono-first.test.ts +63 -0
  20. package/src/build-prod-bundle.ts +587 -0
  21. package/src/build-server-bundle.ts +308 -0
  22. package/src/build.ts +28 -0
  23. package/src/codegen/__tests__/run-codegen.test.ts +494 -0
  24. package/src/codegen/__tests__/strict-mode-diagnostics.test.ts +467 -0
  25. package/src/codegen/__tests__/watch.test.ts +186 -0
  26. package/src/codegen/index.ts +17 -0
  27. package/src/codegen/render.ts +225 -0
  28. package/src/codegen/run-codegen.ts +157 -0
  29. package/src/codegen/scan-events.ts +574 -0
  30. package/src/codegen/watch.ts +127 -0
  31. package/src/compose-features.ts +128 -0
  32. package/src/crash-tracker.ts +56 -0
  33. package/src/create-kumiko-server.ts +1010 -0
  34. package/src/drizzle-config.ts +44 -0
  35. package/src/drizzle-tables-auth-mode.ts +32 -0
  36. package/src/drizzle-tables-minimal.ts +22 -0
  37. package/src/few-shot-corpus.ts +369 -0
  38. package/src/index.ts +57 -0
  39. package/src/inject-schema.ts +24 -0
  40. package/src/resolve-tailwind-cli.ts +28 -0
  41. package/src/run-dev-app.ts +290 -0
  42. package/src/run-prod-app.ts +892 -0
  43. package/src/scaffold-feature.ts +226 -0
  44. package/src/try-hono-first.ts +46 -0
@@ -0,0 +1,44 @@
1
+ // kumikoDrizzleConfig — Convention-Helper für drizzle.config.ts in App-
2
+ // Workspaces. Convention-driven Defaults statt Boilerplate-Copy:
3
+ //
4
+ // import { kumikoDrizzleConfig } from "@cosmicdrift/kumiko-dev-server/drizzle-config";
5
+ // export default kumikoDrizzleConfig();
6
+ //
7
+ // Default-Pfade:
8
+ // schema: "./drizzle/schema.ts"
9
+ // out: "./drizzle/migrations"
10
+ // db url: process.env[DATABASE_URL] (override via options)
11
+ // dialect: "postgresql"
12
+ // verbose + strict: true (drizzle-kit-Defaults für Production-Workflow)
13
+ //
14
+ // Apps mit untypischer Verzeichnis-Struktur können einzelne Werte
15
+ // überschreiben, der Rest bleibt Convention.
16
+
17
+ import { defineConfig } from "drizzle-kit";
18
+
19
+ export type KumikoDrizzleConfigOptions = {
20
+ /** Pfad zum Schema-Barrel relativ zum App-Root. Default: "./drizzle/schema.ts". */
21
+ readonly schemaPath?: string;
22
+ /** Migrations-Out-Folder relativ zum App-Root. Default: "./drizzle/migrations". */
23
+ readonly outDir?: string;
24
+ /** Env-Var-Name für die Database-URL. Default: "DATABASE_URL". */
25
+ readonly databaseUrlEnv?: string;
26
+ /** Fallback-URL wenn die Env-Var leer ist (für lokale Dev-Setups).
27
+ * Default: postgres://kumiko:kumiko@localhost:15432/kumiko_dev (kumiko dev-stack). */
28
+ readonly fallbackDatabaseUrl?: string;
29
+ };
30
+
31
+ export function kumikoDrizzleConfig(options: KumikoDrizzleConfigOptions = {}) {
32
+ const envName = options.databaseUrlEnv ?? "DATABASE_URL";
33
+ const fallback =
34
+ options.fallbackDatabaseUrl ?? "postgres://kumiko:kumiko@localhost:15432/kumiko_dev";
35
+ const url = process.env[envName] ?? fallback;
36
+ return defineConfig({
37
+ schema: options.schemaPath ?? "./drizzle/schema.ts",
38
+ out: options.outDir ?? "./drizzle/migrations",
39
+ dialect: "postgresql",
40
+ dbCredentials: { url },
41
+ verbose: true,
42
+ strict: true,
43
+ });
44
+ }
@@ -0,0 +1,32 @@
1
+ // Drizzle-Schema-Barrel: Framework-Infrastruktur-Tables.
2
+ //
3
+ // Re-exportiert die Framework-eigenen Tables (Event-Store + Pipeline-State)
4
+ // die jede App in der DB haben muss. App-Author schreibt:
5
+ //
6
+ // // drizzle/schema.ts
7
+ // export * from "@cosmicdrift/kumiko-dev-server/drizzle-tables-auth-mode";
8
+ // export * from "./schema.generated"; // App-eigene + Bundle-Entity-Tables
9
+ //
10
+ // drizzle-kit sucht im schema-Module nur nach pgTable-Instances und
11
+ // ignoriert alle anderen exports — `export *` ist hier sicher.
12
+ //
13
+ // Bundle-Entity-Tables (configValuesTable, tenantTable, userTable,
14
+ // userSessionTable, tenantMembershipsTable etc.) sind bewusst NICHT hier:
15
+ // sie kommen über schema.generated.ts via buildDrizzleTable aus den
16
+ // r.entity()-Definitionen, das ist seit der entity.indexes-API die
17
+ // Single-Source-of-Truth. Doppelte Re-Exports würden zwei pgTable-
18
+ // Instances mit identischem Index-Namen erzeugen — drizzle-kit warnt.
19
+ //
20
+ // (Datei-Name "auth-mode" ist historisch — heute ist der Inhalt reine
21
+ // Framework-Infra. Umbenennen ist breaking change für App-Imports.)
22
+
23
+ // Framework-Infra (immer da, unabhängig von Features)
24
+ export {
25
+ archivedStreamsTable,
26
+ eventsTable,
27
+ snapshotsTable,
28
+ } from "@cosmicdrift/kumiko-framework/event-store";
29
+ export {
30
+ eventConsumerStateTable,
31
+ projectionStateTable,
32
+ } from "@cosmicdrift/kumiko-framework/pipeline";
@@ -0,0 +1,22 @@
1
+ // Drizzle-Schema-Barrel: Minimal (nur Framework-Infra, kein Auth).
2
+ //
3
+ // Apps OHNE Auth-Stack (anonymous-only, embedded Demos, Headless-APIs)
4
+ // brauchen nur die Framework-Infrastructure-Tables — Event-Store +
5
+ // Pipeline-State. Verwendung in drizzle/schema.ts:
6
+ //
7
+ // export * from "@cosmicdrift/kumiko-dev-server/drizzle-tables-minimal";
8
+ // export * from "./schema.generated"; // App-eigene Entity-Tables
9
+ //
10
+ // Wer Auth-Tables will, nutzt drizzle-tables-auth-mode (umfasst Minimal
11
+ // plus Bundle-Tables für Config, Tenant, User, Sessions).
12
+
13
+ export {
14
+ archivedStreamsTable,
15
+ eventsTable,
16
+ snapshotsTable,
17
+ upcasterDeadLetterTable,
18
+ } from "@cosmicdrift/kumiko-framework/event-store";
19
+ export {
20
+ eventConsumerStateTable,
21
+ projectionStateTable,
22
+ } from "@cosmicdrift/kumiko-framework/pipeline";
@@ -0,0 +1,369 @@
1
+ // buildFewShotCorpus — extracts feature-files from the repo into a
2
+ // structured JSON corpus that L2 (Prompt-Pipeline) feeds into the LLM
3
+ // at generation time. One entry per feature-file, four data sources
4
+ // per entry: raw source text, package.json description, parsed
5
+ // FeaturePattern[], plus pattern-categories from the C4 library.
6
+ //
7
+ // **Why a checked-in JSON file (docs/few-shot-corpus.json):**
8
+ // - Offline-consumable: AI-Builder can prompt without re-parsing
9
+ // 30+ feature files at request time.
10
+ // - Diff-reviewable: when a feature changes, the corpus diff shows
11
+ // up in the PR — humans see how the example surface shifted.
12
+ // - Portable: the corpus is a flat JSON, can ship to a hosted LLM
13
+ // service / fine-tuning pipeline / customer environment without
14
+ // dragging the framework's parser along.
15
+ //
16
+ // **Authoring-Style classification:**
17
+ // - `canonical` → 0 ParseErrors. The example is "do it like this"
18
+ // for the LLM.
19
+ // - `legacy` → has ParseErrors. The example shows what code looks
20
+ // like in the wild (Factory-style, identifier-refs) and is useful
21
+ // for understanding intent — but the LLM should NOT replicate
22
+ // this style. L2 marks legacy entries as "do not generate".
23
+ //
24
+ // **Why the corpus includes legacy entries but the Designer does not:**
25
+ // The two consumers want different slices of the same data. L2 needs
26
+ // counter-examples (showing the LLM what *not* to emit raises the
27
+ // chance of clean output), so legacy entries are kept and tagged.
28
+ // The Designer can only round-trip canonical-form patterns through
29
+ // the AST-Patcher — legacy entries would render as read-only with
30
+ // no edit affordance, which is worse than hiding them. So the corpus
31
+ // builder is permissive, and the Designer filters at read-time.
32
+
33
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
34
+ import { dirname, join, relative, resolve } from "node:path";
35
+ import {
36
+ type FeaturePattern,
37
+ type FeaturePatternKind,
38
+ PATTERN_LIBRARY,
39
+ type ParseError,
40
+ parseFeatureFile,
41
+ } from "@cosmicdrift/kumiko-framework/engine";
42
+
43
+ // =============================================================================
44
+ // Public types
45
+ // =============================================================================
46
+
47
+ export type AuthoringStyle = "canonical" | "legacy";
48
+
49
+ export type CorpusWarning = {
50
+ /** Repo-relative path to the file that triggered the warning. */
51
+ readonly sourcePath: string;
52
+ /** Human-readable explanation. Currently only "parser-throw" but
53
+ * kept open-ended so future builders can add more (e.g.
54
+ * "duplicate-id", "no-feature-name"). */
55
+ readonly reason: string;
56
+ };
57
+
58
+ export type FewShotEntry = {
59
+ /** Stable id derived from the path (`samples/recipes/basic-entity`). */
60
+ readonly id: string;
61
+ /** Repo-relative path to the feature-file. */
62
+ readonly sourcePath: string;
63
+ /** Repo-relative path to the workspace package.json (for description lookup). */
64
+ readonly packageJsonPath: string | undefined;
65
+ /** Workspace name (`@cosmicdrift/kumiko-sample-basic-entity`). */
66
+ readonly packageName: string | undefined;
67
+ /** English description from package.json. */
68
+ readonly description: string | undefined;
69
+ /** Feature name from `defineFeature("...", ...)`. Undefined if the
70
+ * parser couldn't read it (rare — implies a non-feature file got
71
+ * picked up). */
72
+ readonly featureName: string | undefined;
73
+ /** Pattern-categories present in this feature, deduplicated. Useful
74
+ * as topic-tags for retrieval ("show me cross-cutting examples"). */
75
+ readonly tags: readonly string[];
76
+ /** Counts per pattern-kind — quick stats without scanning the
77
+ * patterns array. */
78
+ readonly patternsByKind: Readonly<Record<string, number>>;
79
+ /** All parsed patterns. Same shape the parser emits. */
80
+ readonly patterns: readonly FeaturePattern[];
81
+ /** Errors the parser raised. Empty for canonical-form features. */
82
+ readonly parseErrors: readonly ParseError[];
83
+ /** `canonical` (clean parse) or `legacy` (parser couldn't read it
84
+ * fully — Factory-style / identifier-refs). */
85
+ readonly authoringStyle: AuthoringStyle;
86
+ /** Raw source text — kept verbatim so the LLM can train on the
87
+ * exact byte-form, including comments + whitespace. */
88
+ readonly rawSource: string;
89
+ };
90
+
91
+ export type FewShotCorpus = {
92
+ /** ISO-instant when the corpus was generated. */
93
+ readonly generatedAt: string;
94
+ /** Total entry count, broken down by authoringStyle for quick stats. */
95
+ readonly totals: {
96
+ readonly all: number;
97
+ readonly canonical: number;
98
+ readonly legacy: number;
99
+ };
100
+ readonly entries: readonly FewShotEntry[];
101
+ /** Files that were discovered but couldn't be turned into entries.
102
+ * Surfaces parser crashes + duplicate-id collisions instead of
103
+ * swallowing them — the regenerate-script reports these to the user
104
+ * and the drift-test asserts the count stays constant. */
105
+ readonly warnings: readonly CorpusWarning[];
106
+ };
107
+
108
+ export type BuildFewShotCorpusOptions = {
109
+ /** Repo root — used for relative-path output and security guards. */
110
+ readonly repoRoot: string;
111
+ /** Folders to scan recursively. Defaults to samples/* + bundled-features. */
112
+ readonly scanRoots?: readonly string[];
113
+ };
114
+
115
+ // =============================================================================
116
+ // Builder
117
+ // =============================================================================
118
+
119
+ const FEATURE_FILE_PATTERN = /(?:^|\/)(feature|.*\.feature)\.ts$/;
120
+
121
+ const DEFAULT_SCAN_ROOTS: readonly string[] = [
122
+ "samples/recipes",
123
+ "samples/apps",
124
+ "samples/showcases",
125
+ "packages/bundled-features/src",
126
+ ];
127
+
128
+ // Static timestamp keeps the JSON output deterministic across CI runs —
129
+ // the regenerate-script could overwrite this with a real timestamp,
130
+ // but drift-tests compare structural data, not timestamps. Centralized
131
+ // here so the build path and any future inspector use the same value.
132
+ const STATIC_GENERATED_AT = "1970-01-01T00:00:00Z";
133
+
134
+ export function buildFewShotCorpus(options: BuildFewShotCorpusOptions): FewShotCorpus {
135
+ const repoRoot = resolve(options.repoRoot);
136
+ const scanRoots = (options.scanRoots ?? DEFAULT_SCAN_ROOTS).map((r) => resolve(repoRoot, r));
137
+
138
+ const featureFiles: string[] = [];
139
+ for (const root of scanRoots) {
140
+ if (!existsSync(root)) continue;
141
+ walkDir(root, featureFiles);
142
+ }
143
+ featureFiles.sort();
144
+
145
+ const entries: FewShotEntry[] = [];
146
+ const warnings: CorpusWarning[] = [];
147
+ const seenIds = new Map<string, string>();
148
+
149
+ for (const filePath of featureFiles) {
150
+ const result = buildEntry(filePath, repoRoot);
151
+ if (result.warning) {
152
+ warnings.push(result.warning);
153
+ continue;
154
+ }
155
+ const entry = result.entry;
156
+ const previousPath = seenIds.get(entry.id);
157
+ if (previousPath) {
158
+ // Two feature-files mapped to the same id. The corpus uses ids
159
+ // for retrieval — duplicates would silently overwrite each other
160
+ // in any consumer that built a Map<id, entry>. Surface as a
161
+ // warning, drop the second occurrence.
162
+ warnings.push({
163
+ sourcePath: entry.sourcePath,
164
+ reason: `duplicate-id: collides with ${previousPath}`,
165
+ });
166
+ continue;
167
+ }
168
+ seenIds.set(entry.id, entry.sourcePath);
169
+ entries.push(entry);
170
+ }
171
+
172
+ const canonical = entries.filter((e) => e.authoringStyle === "canonical").length;
173
+ return {
174
+ generatedAt: STATIC_GENERATED_AT,
175
+ totals: {
176
+ all: entries.length,
177
+ canonical,
178
+ legacy: entries.length - canonical,
179
+ },
180
+ entries,
181
+ warnings,
182
+ };
183
+ }
184
+
185
+ type BuildEntryResult =
186
+ | { readonly entry: FewShotEntry; readonly warning?: never }
187
+ | { readonly entry?: never; readonly warning: CorpusWarning };
188
+
189
+ function buildEntry(filePath: string, repoRoot: string): BuildEntryResult {
190
+ const sourcePath = relative(repoRoot, filePath);
191
+
192
+ let parsed: ReturnType<typeof parseFeatureFile>;
193
+ try {
194
+ parsed = parseFeatureFile(filePath);
195
+ } catch (err) {
196
+ // ts-morph couldn't read the file (syntax-error, IO problem, weird
197
+ // encoding). Skip the entry but record *why* — silent skip used to
198
+ // hide newly broken feature-files until L2 hit them.
199
+ const detail = err instanceof Error ? err.message : String(err);
200
+ return {
201
+ warning: { sourcePath, reason: `parser-throw: ${detail}` },
202
+ };
203
+ }
204
+
205
+ const rawSource = readFileSync(filePath, "utf8");
206
+ const id = pathToId(sourcePath);
207
+
208
+ const pkgInfo = findPackageJson(filePath, repoRoot);
209
+
210
+ const tags = collectTags(parsed.patterns);
211
+ const patternsByKind = countPatternsByKind(parsed.patterns);
212
+
213
+ const authoringStyle: AuthoringStyle = parsed.errors.length === 0 ? "canonical" : "legacy";
214
+
215
+ // SourceLocation.file is an absolute path coming out of the parser —
216
+ // strip it to repo-relative so the corpus diff stays stable across
217
+ // machines / CI runners. Same for ParseError.source.file.
218
+ const patterns = parsed.patterns.map((p) => relativizeSources(p, repoRoot) as FeaturePattern);
219
+ const parseErrors = parsed.errors.map((e) => ({
220
+ ...e,
221
+ source: { ...e.source, file: relative(repoRoot, e.source.file) },
222
+ }));
223
+
224
+ return {
225
+ entry: {
226
+ id,
227
+ sourcePath,
228
+ packageJsonPath: pkgInfo?.relPath,
229
+ packageName: pkgInfo?.name,
230
+ description: pkgInfo?.description,
231
+ featureName: parsed.featureName,
232
+ tags,
233
+ patternsByKind,
234
+ patterns,
235
+ parseErrors,
236
+ authoringStyle,
237
+ rawSource,
238
+ },
239
+ };
240
+ }
241
+
242
+ /**
243
+ * Recursively walk a value and rewrite every nested SourceLocation's
244
+ * `file` field to be repo-relative. Identifies SourceLocation by
245
+ * structural shape (`{ file, start, end, raw }`) rather than by
246
+ * type-tag — the parsed objects are plain JSON-ish at this point and
247
+ * a discriminator would complicate the renderer.
248
+ *
249
+ * Typed `unknown → unknown` so the walker stays honest about what it
250
+ * sees. The single boundary cast lives at the call site
251
+ * (`as FeaturePattern`) where the input contract is known.
252
+ */
253
+ function relativizeSources(value: unknown, repoRoot: string): unknown {
254
+ if (Array.isArray(value)) {
255
+ return value.map((v) => relativizeSources(v, repoRoot));
256
+ }
257
+ if (value && typeof value === "object") {
258
+ const obj = value as Record<string, unknown>;
259
+ if (
260
+ typeof obj["file"] === "string" &&
261
+ typeof obj["raw"] === "string" &&
262
+ typeof obj["start"] === "object" &&
263
+ typeof obj["end"] === "object"
264
+ ) {
265
+ return { ...obj, file: relative(repoRoot, obj["file"]) };
266
+ }
267
+ const out: Record<string, unknown> = {};
268
+ for (const [k, v] of Object.entries(obj)) {
269
+ out[k] = relativizeSources(v, repoRoot);
270
+ }
271
+ return out;
272
+ }
273
+ return value;
274
+ }
275
+
276
+ // =============================================================================
277
+ // Helpers
278
+ // =============================================================================
279
+
280
+ /**
281
+ * Stable id from the source path. Drops the leading `samples/` or
282
+ * `packages/` prefix and the trailing `/src/feature.ts` suffix so the
283
+ * id reads like the canonical short name (`basic-entity`,
284
+ * `bundled-features/auth-email-password`).
285
+ */
286
+ export function pathToId(sourcePath: string): string {
287
+ return sourcePath
288
+ .replace(/^(samples|packages)\//, "")
289
+ .replace(/\/src\/feature\.ts$/, "")
290
+ .replace(/\/feature\.ts$/, "");
291
+ }
292
+
293
+ function walkDir(dir: string, acc: string[]): void {
294
+ let entries: { name: string; isDirectory: () => boolean; isFile: () => boolean }[];
295
+ try {
296
+ entries = readdirSync(dir, { withFileTypes: true });
297
+ } catch {
298
+ return;
299
+ }
300
+ for (const entry of entries) {
301
+ const name = String(entry.name);
302
+ const full = join(dir, name);
303
+ if (entry.isDirectory()) {
304
+ if (name === "node_modules" || name === "dist" || name === "dist-server") continue;
305
+ if (name.startsWith(".")) continue;
306
+ walkDir(full, acc);
307
+ } else if (entry.isFile() && FEATURE_FILE_PATTERN.test(full)) {
308
+ acc.push(full);
309
+ }
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Walk upward from `featureFile` to find the nearest `package.json` and
315
+ * pluck name + description out of it. Stops at `repoRoot`.
316
+ */
317
+ function findPackageJson(
318
+ featureFile: string,
319
+ repoRoot: string,
320
+ ): { relPath: string; name: string | undefined; description: string | undefined } | undefined {
321
+ let dir = dirname(featureFile);
322
+ const stopAt = resolve(repoRoot);
323
+ while (true) {
324
+ const candidate = join(dir, "package.json");
325
+ if (existsSync(candidate)) {
326
+ try {
327
+ const pkg = JSON.parse(readFileSync(candidate, "utf8")) as {
328
+ name?: unknown;
329
+ description?: unknown;
330
+ };
331
+ return {
332
+ relPath: relative(repoRoot, candidate),
333
+ name: typeof pkg.name === "string" ? pkg.name : undefined,
334
+ description: typeof pkg.description === "string" ? pkg.description : undefined,
335
+ };
336
+ } catch {
337
+ // Malformed package.json — keep walking up.
338
+ }
339
+ }
340
+ const parent = dirname(dir);
341
+ if (parent === dir || dir === stopAt) return undefined;
342
+ dir = parent;
343
+ }
344
+ }
345
+
346
+ /**
347
+ * Collect the union of pattern-categories present in the feature.
348
+ * Categories come from the pattern-library — the same vocabulary the
349
+ * Designer / AI-Builder uses for filtering ("show me background-jobs
350
+ * examples").
351
+ */
352
+ function collectTags(patterns: readonly FeaturePattern[]): readonly string[] {
353
+ const tags = new Set<string>();
354
+ for (const p of patterns) {
355
+ const schema = PATTERN_LIBRARY[p.kind as FeaturePatternKind];
356
+ if (schema) tags.add(schema.category);
357
+ }
358
+ return [...tags].sort();
359
+ }
360
+
361
+ function countPatternsByKind(
362
+ patterns: readonly FeaturePattern[],
363
+ ): Readonly<Record<string, number>> {
364
+ const counts: Record<string, number> = {};
365
+ for (const p of patterns) {
366
+ counts[p.kind] = (counts[p.kind] ?? 0) + 1;
367
+ }
368
+ return counts;
369
+ }
package/src/index.ts ADDED
@@ -0,0 +1,57 @@
1
+ // Public API für den Kumiko-Dev-Server. Zwei Schichten:
2
+ //
3
+ // - createKumikoServer (low-level)
4
+ // Bun.serve-Wrapper der Client-Bundle, /styles.css, AppSchema-
5
+ // Injection, SSE-Reload und einen Auto-Mint-JWT-Modus liefert. Nimmt
6
+ // features + clientEntry direkt an, kein Auth-Auto-Wiring. Wer
7
+ // einen ungewöhnlichen Auth-Setup braucht (alternative Membership-
8
+ // Query, eigener Rate-Limiter, custom Login-Routes) geht hier rein.
9
+ //
10
+ // - runDevApp (high-level)
11
+ // Mischt die Standard-Features (config/user/tenant/auth-email-
12
+ // password) automatisch dazu wenn `auth` gesetzt ist, wired die
13
+ // Login-Routes + Error-Map, ruft seedAdmin im onAfterSetup. Default
14
+ // für Sample-Apps und Showcases — 5-10 Zeilen Bootstrap statt 50.
15
+
16
+ // Build-Toolchain (buildProdBundle, Bun.build, Tailwind-Pipeline, ts-morph) lebt
17
+ // im Sub-Path-Export `@cosmicdrift/kumiko-dev-server/build`. Damit zieht der Main-Barrel
18
+ // kein Bun-Toolchain-Bundle mehr in Production-Reads (z.B. wenn drizzle-kit
19
+ // die App-Config unter Node lädt).
20
+ export {
21
+ type CodegenOptions,
22
+ type CodegenResult,
23
+ runCodegen,
24
+ type ScannedEvent,
25
+ type ScanWarning,
26
+ scanEvents,
27
+ } from "./codegen";
28
+ export { type ComposeFeaturesOptions, composeFeatures } from "./compose-features";
29
+ export {
30
+ type CreateKumikoServerOptions,
31
+ createKumikoServer,
32
+ type KumikoServerHandle,
33
+ resolveStylesheet,
34
+ } from "./create-kumiko-server";
35
+ export type {
36
+ AuthoringStyle,
37
+ BuildFewShotCorpusOptions,
38
+ CorpusWarning,
39
+ FewShotCorpus,
40
+ FewShotEntry,
41
+ } from "./few-shot-corpus";
42
+ export { buildFewShotCorpus, pathToId } from "./few-shot-corpus";
43
+ export type { RunDevAppAuthOptions, RunDevAppOptions, SeedFn } from "./run-dev-app";
44
+ export { runDevApp } from "./run-dev-app";
45
+ export type {
46
+ EmailVerificationSetup,
47
+ InviteSetup,
48
+ PasswordResetSetup,
49
+ ProdAppHandle,
50
+ ProdSeedFn,
51
+ RunProdAppAuthOptions,
52
+ RunProdAppOptions,
53
+ SignupSetup,
54
+ } from "./run-prod-app";
55
+ export { runProdApp } from "./run-prod-app";
56
+ export type { ScaffoldFeatureOptions, ScaffoldFeatureResult } from "./scaffold-feature";
57
+ export { scaffoldFeature } from "./scaffold-feature";
@@ -0,0 +1,24 @@
1
+ // Injiziert das Server-aufgelöste AppSchema in das HTML-Template damit
2
+ // createKumikoApp() es synchron unter `window.__KUMIKO_SCHEMA__` vorfindet.
3
+ // JSON ist valides JS — direkt eingebettet, der Browser parsed das
4
+ // Object-Literal nativ.
5
+ //
6
+ // Geteilt zwischen dev-server (Schema kommt frisch aus dem laufenden
7
+ // Prozess) und prod-server (Schema wird beim Boot einmal berechnet und
8
+ // in die statisch ausgelieferte index.html injiziert). Beide Pfade
9
+ // nutzen dieselbe Tag-Form damit `createKumikoApp` den Lookup nicht je
10
+ // nach Kontext anders machen muss.
11
+ //
12
+ // Idempotenz: wenn das HTML schon einen __KUMIKO_SCHEMA__-Marker hat,
13
+ // wird nicht doppelt injected — verhindert dass repeated index.html-
14
+ // Reads (prod) oder bereits-vorbereitete templates (custom CI-builds)
15
+ // stacking-Tags produzieren.
16
+
17
+ export function injectSchema(html: string, schemaJson: string): string {
18
+ if (html.includes("__KUMIKO_SCHEMA__")) return html;
19
+ const tag = `<script>window.__KUMIKO_SCHEMA__=${schemaJson};</script>`;
20
+ if (html.includes('<script src="/client.js"')) {
21
+ return html.replace('<script src="/client.js"', `${tag}<script src="/client.js"`);
22
+ }
23
+ return html.includes("</body>") ? html.replace("</body>", `${tag}</body>`) : html + tag;
24
+ }
@@ -0,0 +1,28 @@
1
+ // Resolved den lokal installierten @tailwindcss/cli-Bin auf seinen
2
+ // absoluten Dateipfad. Wir vermeiden `bunx`, weil das auch bei lokal
3
+ // installiertem Package noch das Registry-Manifest fragt und ohne Netz
4
+ // mit `FailedToOpenSocket` stirbt.
5
+ //
6
+ // Ausgelagert aus create-kumiko-server.ts, damit unter vitest/Node
7
+ // testbar (Bun-Branch + Resolve-Fail-Branch) ohne den Server zu
8
+ // booten.
9
+
10
+ import { resolve } from "node:path";
11
+
12
+ type BunResolver = { resolveSync: (id: string, from: string) => string };
13
+
14
+ export type ResolveTailwindCliDeps = {
15
+ readonly bun?: BunResolver;
16
+ readonly cwd: string;
17
+ };
18
+
19
+ export function resolveTailwindCli(deps: ResolveTailwindCliDeps): string | undefined {
20
+ if (deps.bun === undefined) return undefined;
21
+ try {
22
+ const pkgJsonPath = deps.bun.resolveSync("@tailwindcss/cli/package.json", deps.cwd);
23
+ // bin: { tailwindcss: "./dist/index.mjs" } → absoluter Pfad
24
+ return resolve(pkgJsonPath, "..", "dist", "index.mjs");
25
+ } catch {
26
+ return undefined;
27
+ }
28
+ }