@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.
- package/bin/kumiko-build.ts +85 -0
- package/bin/kumiko-dev.ts +90 -0
- package/package.json +45 -0
- package/src/__tests__/build-prod-bundle.integration.ts +265 -0
- package/src/__tests__/build-prod-bundle.test.ts +262 -0
- package/src/__tests__/cache-headers.test.ts +70 -0
- package/src/__tests__/classify-change.test.ts +87 -0
- package/src/__tests__/compose-features-wiring.integration.ts +352 -0
- package/src/__tests__/compose-features.test.ts +81 -0
- package/src/__tests__/crash-tracker.test.ts +89 -0
- package/src/__tests__/create-kumiko-server.integration.ts +286 -0
- package/src/__tests__/few-shot-corpus.test.ts +311 -0
- package/src/__tests__/inject-schema.test.ts +62 -0
- package/src/__tests__/resolve-stylesheet.test.ts +90 -0
- package/src/__tests__/resolve-tailwind-cli.test.ts +49 -0
- package/src/__tests__/run-prod-app-spec.test.ts +57 -0
- package/src/__tests__/run-prod-app.integration.ts +535 -0
- package/src/__tests__/scaffold-feature.test.ts +143 -0
- package/src/__tests__/try-hono-first.test.ts +63 -0
- package/src/build-prod-bundle.ts +587 -0
- package/src/build-server-bundle.ts +308 -0
- package/src/build.ts +28 -0
- package/src/codegen/__tests__/run-codegen.test.ts +494 -0
- package/src/codegen/__tests__/strict-mode-diagnostics.test.ts +467 -0
- package/src/codegen/__tests__/watch.test.ts +186 -0
- package/src/codegen/index.ts +17 -0
- package/src/codegen/render.ts +225 -0
- package/src/codegen/run-codegen.ts +157 -0
- package/src/codegen/scan-events.ts +574 -0
- package/src/codegen/watch.ts +127 -0
- package/src/compose-features.ts +128 -0
- package/src/crash-tracker.ts +56 -0
- package/src/create-kumiko-server.ts +1010 -0
- package/src/drizzle-config.ts +44 -0
- package/src/drizzle-tables-auth-mode.ts +32 -0
- package/src/drizzle-tables-minimal.ts +22 -0
- package/src/few-shot-corpus.ts +369 -0
- package/src/index.ts +57 -0
- package/src/inject-schema.ts +24 -0
- package/src/resolve-tailwind-cli.ts +28 -0
- package/src/run-dev-app.ts +290 -0
- package/src/run-prod-app.ts +892 -0
- package/src/scaffold-feature.ts +226 -0
- 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
|
+
}
|