@cosmicdrift/kumiko-dev-server 0.74.0 → 0.76.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-dev-server",
3
- "version": "0.74.0",
3
+ "version": "0.76.0",
4
4
  "description": "Development server bootstrap for Kumiko apps. Bundles the client, mints dev-JWTs, injects the resolved AppSchema, and seeds an admin. Not for production.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -50,8 +50,8 @@
50
50
  "kumiko-schema-check": "./bin/kumiko-schema-check.ts"
51
51
  },
52
52
  "dependencies": {
53
- "@cosmicdrift/kumiko-bundled-features": "0.74.0",
54
- "@cosmicdrift/kumiko-framework": "0.74.0",
53
+ "@cosmicdrift/kumiko-bundled-features": "0.76.0",
54
+ "@cosmicdrift/kumiko-framework": "0.76.0",
55
55
  "ts-morph": "^28.0.0"
56
56
  },
57
57
  "publishConfig": {
@@ -15,7 +15,7 @@ describe("scaffoldApp", () => {
15
15
  rmSync(tmp, { recursive: true, force: true });
16
16
  });
17
17
 
18
- test("scaffolds 6 files into <cwd>/<name>", () => {
18
+ test("scaffolds the expected files into <cwd>/<name>", () => {
19
19
  const dest = join(tmp, "my-shop");
20
20
  const result = scaffoldApp({ name: "my-shop", destination: dest });
21
21
 
@@ -26,6 +26,7 @@ describe("scaffoldApp", () => {
26
26
  "tsconfig.json",
27
27
  "src/run-config.ts",
28
28
  "bin/main.ts",
29
+ "bin/dev.ts",
29
30
  ".env.example",
30
31
  "README.md",
31
32
  ]);
@@ -48,6 +49,26 @@ describe("scaffoldApp", () => {
48
49
  expect(pkg.dependencies["@cosmicdrift/kumiko-dev-server"]).toBe("^0.13.0");
49
50
  expect(pkg.dependencies["@cosmicdrift/kumiko-framework"]).toBe("^0.13.0");
50
51
  expect(pkg.scripts["boot"]).toContain("KUMIKO_DRY_RUN_ENV=boot");
52
+ expect(pkg.scripts["dev"]).toBe("bun --watch bin/dev.ts");
53
+ });
54
+
55
+ test("bin/dev.ts contains runDevApp + welcomeBanner + admin login", () => {
56
+ const dest = join(tmp, "my-shop");
57
+ scaffoldApp({ name: "my-shop", destination: dest });
58
+
59
+ const dev = readFileSync(join(dest, "bin/dev.ts"), "utf-8");
60
+ expect(dev).toContain("runDevApp");
61
+ expect(dev).toContain("welcomeBanner: true");
62
+ expect(dev).toContain("admin@my-shop.local");
63
+ expect(dev).toContain(`password: "changeme"`);
64
+ });
65
+
66
+ test(".env.example carries KUMIKO_DEV_DB_NAME default so reboots are persistent", () => {
67
+ const dest = join(tmp, "my-shop");
68
+ scaffoldApp({ name: "my-shop", destination: dest });
69
+
70
+ const env = readFileSync(join(dest, ".env.example"), "utf-8");
71
+ expect(env).toContain("KUMIKO_DEV_DB_NAME=my_shop_dev");
51
72
  });
52
73
 
53
74
  test("bin/main.ts contains runProdApp + auth.admin stub", () => {
@@ -0,0 +1,109 @@
1
+ // Phase-2 Validator (Plan-Doc create-kumiko-app.md, Risk #1): pinnt dass
2
+ // setupTestStack auf persistent DB einen Reboot mit erweitertem Feature-Set
3
+ // überlebt — eine neue r.entity ohne Migration-CLI muss die Tabelle anlegen,
4
+ // und ein bereits existierendes Entity darf nicht crashen (duplicate CREATE).
5
+ //
6
+ // Das ist der Pfad den `bun dev` mit KUMIKO_DEV_DB_NAME triggert: der
7
+ // Dev-User editiert src/features/notes.ts, `bun --watch` rebootet den Process,
8
+ // runDevApp ruft createKumikoServer → setupTestStack mit denselben DB-Name
9
+ // auf, und das neue Notes-Entity muss als Tabelle erscheinen ohne dass der
10
+ // User `kumiko schema apply` aufgerufen hat. Bricht der Filter in
11
+ // push-entity-projection-tables (test-stack.ts:200-209), lügt der Dev-Flow.
12
+
13
+ import { afterAll, describe, expect, test } from "bun:test";
14
+ import { asRawClient, createDbConnection, tableExists } from "@cosmicdrift/kumiko-framework/db";
15
+ import {
16
+ createEntity,
17
+ createTextField,
18
+ defineFeature,
19
+ type FeatureDefinition,
20
+ } from "@cosmicdrift/kumiko-framework/engine";
21
+ import {
22
+ pushEntityProjectionTables,
23
+ setupTestStack,
24
+ type TestStack,
25
+ } from "@cosmicdrift/kumiko-framework/stack";
26
+ import { generateId } from "@cosmicdrift/kumiko-framework/utils";
27
+
28
+ const baseEntity = createEntity({
29
+ fields: { title: createTextField({ required: true }) },
30
+ table: "phase2_base_thing",
31
+ });
32
+
33
+ const noteEntity = createEntity({
34
+ fields: { title: createTextField({ required: true }) },
35
+ table: "phase2_note",
36
+ });
37
+
38
+ const baseFeature: FeatureDefinition = defineFeature("phase2-base", (r) => {
39
+ r.entity("base-thing", baseEntity);
40
+ });
41
+
42
+ const notesFeature: FeatureDefinition = defineFeature("phase2-notes", (r) => {
43
+ r.entity("note", noteEntity);
44
+ });
45
+
46
+ const dbName = `kumiko_phase2_${generateId().slice(-8)}`;
47
+
48
+ afterAll(async () => {
49
+ const base = process.env["TEST_DATABASE_URL"];
50
+ if (!base) return;
51
+ const adminUrl = base.replace(/\/[^/]+$/, "/postgres");
52
+ const admin = createDbConnection(adminUrl, { maxConnections: 1 });
53
+ try {
54
+ await asRawClient(admin.db).unsafe(`DROP DATABASE IF EXISTS "${dbName}"`);
55
+ } finally {
56
+ await admin.close();
57
+ }
58
+ });
59
+
60
+ describe("scaffold dev cycle (Phase 2 Validator)", () => {
61
+ test("persistent reboot with added entity creates only the new table", async () => {
62
+ const base = process.env["TEST_DATABASE_URL"];
63
+ if (!base) throw new Error("TEST_DATABASE_URL required");
64
+
65
+ // Boot 1: only base feature, persistent DB.
66
+ const stack1: TestStack = await setupTestStack({
67
+ features: [baseFeature],
68
+ dbName,
69
+ persistentDb: true,
70
+ });
71
+ try {
72
+ await pushEntityProjectionTables(stack1, stack1.registry);
73
+ expect(await tableExists(stack1.db, "public.phase2_base_thing")).toBe(true);
74
+ expect(await tableExists(stack1.db, "public.phase2_note")).toBe(false);
75
+ } finally {
76
+ await stack1.cleanup();
77
+ }
78
+
79
+ // Boot 2: base + notes — simulates the Dev-User edit + bun --watch reboot.
80
+ // pushEntityProjectionTables MUSS phase2_base_thing skippen (existiert)
81
+ // und phase2_note neu anlegen — sonst crash auf duplicate CREATE TABLE
82
+ // oder die Notes-Tabelle fehlt.
83
+ const stack2: TestStack = await setupTestStack({
84
+ features: [baseFeature, notesFeature],
85
+ dbName,
86
+ persistentDb: true,
87
+ });
88
+ try {
89
+ await pushEntityProjectionTables(stack2, stack2.registry);
90
+ expect(await tableExists(stack2.db, "public.phase2_base_thing")).toBe(true);
91
+ expect(await tableExists(stack2.db, "public.phase2_note")).toBe(true);
92
+ } finally {
93
+ await stack2.cleanup();
94
+ }
95
+
96
+ // Boot 3 with same feature-set must remain idempotent (no double-CREATE).
97
+ const stack3: TestStack = await setupTestStack({
98
+ features: [baseFeature, notesFeature],
99
+ dbName,
100
+ persistentDb: true,
101
+ });
102
+ try {
103
+ await pushEntityProjectionTables(stack3, stack3.registry);
104
+ expect(await tableExists(stack3.db, "public.phase2_note")).toBe(true);
105
+ } finally {
106
+ await stack3.cleanup();
107
+ }
108
+ });
109
+ });
@@ -4,7 +4,7 @@
4
4
  // docs without an actual `bunx … && bun install && bun run boot` CI run.
5
5
  //
6
6
  // What this test pins:
7
- // - scaffoldApp produces the 6 files the walkthrough lists
7
+ // - scaffoldApp produces the files the walkthrough lists
8
8
  // - scaffoldAppFeature scaffolds + auto-mounts (the diff-block shown)
9
9
  // - composeFeatures(includeBundled:true) yields the exact feature-count
10
10
  // the walkthrough advertises in "Expected output"
@@ -32,13 +32,14 @@ describe("walkthrough — DX-3.1 snapshot", () => {
32
32
  rmSync(tmp, { recursive: true, force: true });
33
33
  });
34
34
 
35
- test("Step 1 (kumiko new app) — produces walkthrough's 6 files", () => {
35
+ test("Step 1 (kumiko new app) — produces walkthrough's scaffold files", () => {
36
36
  const result = scaffoldApp({ name: "my-notes", destination: appRoot });
37
37
  expect(result.files).toEqual([
38
38
  "package.json",
39
39
  "tsconfig.json",
40
40
  "src/run-config.ts",
41
41
  "bin/main.ts",
42
+ "bin/dev.ts",
42
43
  ".env.example",
43
44
  "README.md",
44
45
  ]);
@@ -80,7 +80,10 @@ export function scaffoldApp(options: ScaffoldAppOptions): ScaffoldAppResult {
80
80
  write(join(destination, "bin", "main.ts"), renderMain(options.name));
81
81
  files.push("bin/main.ts");
82
82
 
83
- write(join(destination, ".env.example"), renderEnvExample());
83
+ write(join(destination, "bin", "dev.ts"), renderDev(options.name));
84
+ files.push("bin/dev.ts");
85
+
86
+ write(join(destination, ".env.example"), renderEnvExample(options.name));
84
87
  files.push(".env.example");
85
88
 
86
89
  write(join(destination, "README.md"), renderReadme(options.name));
@@ -102,6 +105,7 @@ function renderPackageJson(name: string, version: string): string {
102
105
  private: true,
103
106
  type: "module",
104
107
  scripts: {
108
+ dev: "bun --watch bin/dev.ts",
105
109
  boot: "KUMIKO_DRY_RUN_ENV=boot bun bin/main.ts",
106
110
  check: "tsc --noEmit",
107
111
  },
@@ -313,7 +317,81 @@ function renderMain(appName: string): string {
313
317
  return sf.getFullText();
314
318
  }
315
319
 
316
- function renderEnvExample(): string {
320
+ function renderDev(appName: string): string {
321
+ const tenantId = deriveTenantId(appName);
322
+ const project = newTsProject();
323
+ const sf = project.createSourceFile("dev.ts", "");
324
+
325
+ sf.addImportDeclaration({
326
+ moduleSpecifier: "@cosmicdrift/kumiko-dev-server",
327
+ namedImports: ["runDevApp"],
328
+ });
329
+ sf.addImportDeclaration({
330
+ moduleSpecifier: "@cosmicdrift/kumiko-framework/engine",
331
+ isTypeOnly: true,
332
+ namedImports: ["TenantId"],
333
+ });
334
+ sf.addImportDeclaration({
335
+ moduleSpecifier: "../src/run-config",
336
+ namedImports: ["APP_FEATURES"],
337
+ });
338
+
339
+ sf.addVariableStatement({
340
+ declarationKind: VariableDeclarationKind.Const,
341
+ declarations: [
342
+ {
343
+ name: "DEFAULT_TENANT_ID",
344
+ initializer: `"${tenantId}" as TenantId`,
345
+ },
346
+ ],
347
+ });
348
+
349
+ sf.addStatements((writer) => {
350
+ writer
351
+ .write("await runDevApp(")
352
+ .inlineBlock(() => {
353
+ writer.writeLine("features: APP_FEATURES,");
354
+ writer.writeLine("welcomeBanner: true,");
355
+ writer.write("auth: ").inlineBlock(() => {
356
+ writer.write("admin: ").inlineBlock(() => {
357
+ writer.writeLine(`email: "admin@${appName}.local",`);
358
+ writer.writeLine(`password: "changeme",`);
359
+ writer.writeLine(`displayName: "Admin",`);
360
+ writer.write("memberships: [");
361
+ writer.indent(() => {
362
+ writer.inlineBlock(() => {
363
+ writer.writeLine("tenantId: DEFAULT_TENANT_ID,");
364
+ writer.writeLine(`tenantKey: "${appName}",`);
365
+ writer.writeLine(`tenantName: "${appName}",`);
366
+ writer.writeLine(`roles: ["TenantAdmin"],`);
367
+ });
368
+ writer.write(",");
369
+ });
370
+ writer.write("],");
371
+ });
372
+ });
373
+ })
374
+ .write(");");
375
+ });
376
+
377
+ sf.insertText(
378
+ 0,
379
+ [
380
+ "// Dev-bootstrap. `bun --watch bin/dev.ts` (siehe package.json scripts.dev)",
381
+ "// startet einen full-featured Dev-Server mit Auto-Reload bei Code-Änderungen.",
382
+ "// setupTestStack legt fehlende Entity-Tabellen automatisch an — neues",
383
+ "// r.entity(...) in einem Feature führt beim nächsten Reboot zu CREATE TABLE,",
384
+ "// kein manuelles `kumiko schema apply` nötig (das gilt nur für Prod).",
385
+ "// Persistent-DB via KUMIKO_DEV_DB_NAME (.env) damit Admin + Daten Reboots überleben.",
386
+ "",
387
+ "",
388
+ ].join("\n"),
389
+ );
390
+
391
+ return sf.getFullText();
392
+ }
393
+
394
+ function renderEnvExample(appName: string): string {
317
395
  return `# Required env-vars für boot-mode + dev. Production: über Pulumi/k8s-Secrets.
318
396
  DATABASE_URL=postgres://postgres:postgres@127.0.0.1:5432/app
319
397
  REDIS_URL=redis://127.0.0.1:6379
@@ -324,37 +402,49 @@ JWT_SECRET=change-me-min-32-chars-change-me-min-32
324
402
  # KUMIKO_SECRETS_MASTER_KEY_V1: base64-encoded 32 bytes (AES-256 KEK).
325
403
  # Generate with: openssl rand -base64 32
326
404
  KUMIKO_SECRETS_MASTER_KEY_V1=
405
+
406
+ # Dev-only: persistente DB für \`bun dev\`. Ohne diesen Var startet jeder Reboot
407
+ # eine frische kumiko_test_<random>-DB → Admin-Login + Daten weg bei jedem Edit.
408
+ # Mit Var bleibt die DB zwischen Reboots erhalten (Schema-Pushes sind idempotent).
409
+ KUMIKO_DEV_DB_NAME=${appName.replace(/-/g, "_")}_dev
327
410
  `;
328
411
  }
329
412
 
330
413
  function renderReadme(appName: string): string {
331
414
  return `# ${appName}
332
415
 
333
- Scaffolded by \`kumiko new app\`. Boots out-of-the-box with secrets + sessions
334
- mounted (foundation set). Add features with \`bunx @cosmicdrift/kumiko-cli add feature <name>\`.
416
+ Scaffolded by \`bun create kumiko-app\`. Boots out-of-the-box with the picked
417
+ feature stack mounted. Add features by editing \`src/run-config.ts\` or via
418
+ \`bunx @cosmicdrift/kumiko-cli add feature <name>\`.
335
419
 
336
- ## First boot
420
+ ## First run
337
421
 
338
422
  \`\`\`sh
339
423
  bun install
340
424
  cp .env.example .env
341
- # edit .env — set JWT_SECRET + KUMIKO_SECRETS_MASTER_KEY_V1
342
- bun run boot
425
+ # edit .env — set JWT_SECRET + KUMIKO_SECRETS_MASTER_KEY_V1, point DATABASE_URL/REDIS_URL at a real PG+Redis
426
+ docker compose up -d # if you don't have PG+Redis running already
427
+ bun dev
343
428
  \`\`\`
344
429
 
345
- Expected: \`[runProdApp] boot validation OK (… features, registry entries)\` + exit 0.
430
+ The dev-server prints a welcome banner with the URL + admin login when ready.
431
+ Edits to \`src/features/**\` trigger a process restart (\`bun --watch\`); new
432
+ \`r.entity(...)\` calls auto-create tables on reboot — no manual migration.
346
433
 
347
- ## Adding features
434
+ ## Boot-only smoke (no DB needed)
348
435
 
349
436
  \`\`\`sh
350
- bunx @cosmicdrift/kumiko-cli add feature my-domain
351
- # → editiert src/run-config.ts automatisch + scaffolded src/features/my-domain/
437
+ bun run boot
352
438
  \`\`\`
353
439
 
440
+ Runs \`KUMIKO_DRY_RUN_ENV=boot bun bin/main.ts\` — validates feature composition
441
+ + env schema, exits 0 without touching DB/Redis. Useful in CI.
442
+
354
443
  ## Architecture
355
444
 
356
445
  - \`src/run-config.ts\` — single source of truth: which features your app mounts.
357
- - \`bin/main.ts\` — production-bootstrap. Reads env, mounts features, starts server.
446
+ - \`bin/dev.ts\` — dev-server entry (\`bun dev\`).
447
+ - \`bin/main.ts\` — production-bootstrap (\`bun run boot\` smoke + production deploy).
358
448
 
359
449
  For full docs see https://docs.kumiko.rocks.
360
450
  `;