@cosmicdrift/kumiko-dev-server 0.369.0 → 0.370.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 (43) hide show
  1. package/dist/build.d.ts +1 -1
  2. package/dist/cli.d.ts +3 -3
  3. package/dist/compose-stacks.d.ts +57 -3
  4. package/dist/compose-stacks.js +88 -9
  5. package/dist/host-dispatch-adapter.d.ts +7 -0
  6. package/dist/host-dispatch-adapter.js +31 -0
  7. package/dist/index.d.ts +21 -20
  8. package/dist/index.js +2 -1
  9. package/dist/init-deploy-cli.js +6 -0
  10. package/dist/managed-files/app-scripts.d.ts +13 -0
  11. package/dist/managed-files/app-scripts.js +40 -0
  12. package/dist/managed-files/check.d.ts +34 -0
  13. package/dist/managed-files/check.js +109 -0
  14. package/dist/managed-files/index.d.ts +8 -0
  15. package/dist/managed-files/index.js +8 -0
  16. package/dist/managed-files/jsonc.d.ts +1 -0
  17. package/dist/managed-files/jsonc.js +12 -0
  18. package/dist/managed-files/line-diff.d.ts +2 -0
  19. package/dist/managed-files/line-diff.js +60 -0
  20. package/dist/managed-files/marker.d.ts +17 -0
  21. package/dist/managed-files/marker.js +49 -0
  22. package/dist/managed-files/port-registry.d.ts +17 -0
  23. package/dist/managed-files/port-registry.js +31 -0
  24. package/dist/managed-files/registry.d.ts +51 -0
  25. package/dist/managed-files/registry.js +115 -0
  26. package/dist/managed-files/static-app-config.d.ts +16 -0
  27. package/dist/managed-files/static-app-config.js +99 -0
  28. package/dist/managed-files/sync.d.ts +19 -0
  29. package/dist/managed-files/sync.js +101 -0
  30. package/dist/run-dev-app.d.ts +1 -1
  31. package/dist/run-dev-app.js +7 -1
  32. package/dist/scaffold-app.d.ts +3 -3
  33. package/dist/scaffold-app.js +125 -199
  34. package/dist/scaffold-deploy.d.ts +9 -0
  35. package/dist/scaffold-deploy.js +34 -6
  36. package/dist/scaffold-feature.js +1 -0
  37. package/package.json +18 -9
  38. package/src/changes.json +19 -0
  39. package/templates/managed/cache-cleanup.yml.template +30 -0
  40. package/templates/managed/ci.yml.template +65 -0
  41. package/templates/managed/husky-pre-push.template +36 -0
  42. package/templates/managed/kumiko-schema.ts.template +12 -0
  43. package/templates/managed/release.yml.template +55 -0
@@ -1,22 +1,31 @@
1
1
  // scaffoldApp — generate a runnable Kumiko app workspace from a name.
2
2
  //
3
3
  // Used by `kumiko new app <name>`. Produces the minimal app shape that
4
- // `KUMIKO_DRY_RUN_ENV=boot bun bin/main.ts` runs successfully against:
5
- // run-config with 5 foundation features, bin/main.ts with auth-admin
6
- // stub, package.json with @cosmicdrift/* deps, tsconfig, .env.example,
7
- // README.
4
+ // `KUMIKO_DRY_RUN_ENV=boot bun bin/main.ts` and `kumiko check` run green
5
+ // against. Every framework-owned file (deploy, workflows, bunfigs, presets,
6
+ // kumiko/schema.ts) comes from the managed-files registry, the same renders
7
+ // `kumiko check managed` compares against.
8
8
  //
9
9
  // Generated files are emitted already formatted to Biome's output (import
10
10
  // order, 100-col wrapping, trailing commas) so a fresh app passes `biome check`.
11
11
  // Static files (package.json, tsconfig, .env, README) stay text-based.
12
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
13
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
14
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
15
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
16
+ });
17
+ }
18
+ return path;
19
+ };
12
20
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
13
- import { dirname, join, relative, resolve } from "node:path";
21
+ import { dirname, join, resolve } from "node:path";
22
+ import { APP_CONFIG_FILE } from "@cosmicdrift/kumiko-framework/config";
14
23
  import { collectTableMetas, generateMigration, writeSnapshotJson, } from "@cosmicdrift/kumiko-framework/db";
15
24
  import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";
16
25
  import { isKebabSegment } from "./kebab.js";
26
+ import { PREPARE_SCRIPT, resolveManagedAppFacts, syncManagedFiles } from "./managed-files/index.js";
17
27
  import { renderAgentsMd, renderClaudeMd, renderMountedFeatureList } from "./scaffold-agent-docs.js";
18
28
  import { createDemoTasksFeature, renderDemoSeedFile, renderDemoTasksFeatureFile, renderDemoTasksI18n, renderDemoTasksIndex, renderDemoTasksWebIndex, } from "./scaffold-demo-tasks.js";
19
- import { scaffoldDeploy } from "./scaffold-deploy.js";
20
29
  // Exact pin (no caret): the generated files are formatted to this Biome's output,
21
30
  // and a scaffold-lint test keeps it equal to the monorepo's own Biome version.
22
31
  export const SCAFFOLD_BIOME_VERSION = "2.5.7";
@@ -44,20 +53,10 @@ export async function scaffoldApp(options) {
44
53
  const files = [];
45
54
  write(join(destination, "package.json"), renderPackageJson(options.name, version, testSetup));
46
55
  files.push("package.json");
47
- write(join(destination, "tsconfig.json"), renderTsconfig());
48
- files.push("tsconfig.json");
49
56
  write(join(destination, "kumiko.json"), renderKumikoManifest());
50
57
  files.push("kumiko.json");
51
- write(join(destination, "biome.json"), renderBiomeJson());
52
- files.push("biome.json");
53
58
  write(join(destination, ".gitignore"), SCAFFOLD_GITIGNORE);
54
59
  files.push(".gitignore");
55
- if (testSetup === undefined) {
56
- write(join(destination, "bunfig.toml"), renderBunfigToml());
57
- files.push("bunfig.toml");
58
- write(join(destination, "bunfig.ci.toml"), renderBunfigCiToml());
59
- files.push("bunfig.ci.toml");
60
- }
61
60
  write(join(destination, "src", "run-config.ts"), renderRunConfig(options.features));
62
61
  files.push("src/run-config.ts");
63
62
  mkdirSync(join(destination, "src", "features", "tasks", "web"), { recursive: true });
@@ -71,12 +70,14 @@ export async function scaffoldApp(options) {
71
70
  files.push("src/features/tasks/web/index.ts");
72
71
  write(join(destination, "src", "seed.ts"), renderDemoSeedFile());
73
72
  files.push("src/seed.ts");
74
- write(join(destination, "kumiko", "schema.ts"), renderKumikoSchema());
75
- files.push("kumiko/schema.ts");
73
+ write(join(destination, APP_CONFIG_FILE), renderAppConfig());
74
+ files.push(APP_CONFIG_FILE);
76
75
  write(join(destination, "bin", "main.ts"), renderMain(options.name));
77
76
  files.push("bin/main.ts");
78
- write(join(destination, "bin", "dev.ts"), renderDev(options.name));
79
- files.push("bin/dev.ts");
77
+ write(join(destination, "bin", "server.ts"), renderServer(options.name));
78
+ files.push("bin/server.ts");
79
+ write(join(destination, "bin", "env-schema.ts"), renderEnvSchema(options.name));
80
+ files.push("bin/env-schema.ts");
80
81
  write(join(destination, "bin", "kumiko.ts"), renderBinKumiko());
81
82
  files.push("bin/kumiko.ts");
82
83
  write(join(destination, "src", "client.tsx"), renderClient(options.name));
@@ -89,12 +90,6 @@ export async function scaffoldApp(options) {
89
90
  files.push("docker-compose.yml");
90
91
  await writeInitMigration(destination, options.features);
91
92
  files.push("kumiko/migrations/0001_init.sql", "kumiko/migrations/.snapshot.json");
92
- const deploy = scaffoldDeploy({ appName: options.name, destination });
93
- for (const f of deploy.files) {
94
- if (f.written) {
95
- files.push(relative(destination, f.path));
96
- }
97
- }
98
93
  if (testSetup !== undefined) {
99
94
  for (const [rel, content] of Object.entries(testSetup.files)) {
100
95
  mkdirSync(dirname(join(destination, rel)), { recursive: true });
@@ -102,6 +97,15 @@ export async function scaffoldApp(options) {
102
97
  files.push(rel);
103
98
  }
104
99
  }
100
+ const managedFacts = resolveManagedAppFacts({
101
+ appRoot: destination,
102
+ appName: options.name,
103
+ uiRoots: SCAFFOLD_UI_ROOTS,
104
+ });
105
+ for (const action of syncManagedFiles(managedFacts, { write: true, adopt: false })) {
106
+ if (action.action === "create")
107
+ files.push(action.path);
108
+ }
105
109
  write(join(destination, "README.md"), `${renderReadme(options.name, options.features)}${testSetup === undefined ? "" : `\n${testSetup.rulesMarkdown}`}`);
106
110
  files.push("README.md");
107
111
  write(join(destination, "AGENTS.md"), renderAgentsMd({
@@ -120,15 +124,17 @@ function write(path, content) {
120
124
  }
121
125
  function appScripts(testSetup) {
122
126
  return {
123
- dev: "bun --watch bin/dev.ts",
127
+ dev: "bun --watch bin/server.ts",
124
128
  build: "bun kumiko-build",
125
129
  start: "bun run bin/main.ts",
126
130
  boot: "KUMIKO_DRY_RUN_ENV=boot bun bin/main.ts",
127
131
  typecheck: "tsc --noEmit",
128
132
  lint: "biome check .",
129
- ...(testSetup?.scripts ?? { test: "bun --config=bunfig.ci.toml test --dots" }),
133
+ check: "kumiko check",
134
+ ...(testSetup?.scripts ?? { test: "bun --config=bunfig.toml test --dots" }),
130
135
  "schema:apply": "bun kumiko-schema apply",
131
136
  "schema:generate": "bun kumiko-schema generate",
137
+ prepare: PREPARE_SCRIPT,
132
138
  };
133
139
  }
134
140
  function renderPackageJson(name, version, testSetup) {
@@ -150,6 +156,7 @@ function renderPackageJson(name, version, testSetup) {
150
156
  },
151
157
  devDependencies: sortedByName({
152
158
  "@biomejs/biome": SCAFFOLD_BIOME_VERSION,
159
+ "@cosmicdrift/kumiko-cli": version,
153
160
  "@tailwindcss/cli": "^4.3.0",
154
161
  "@types/react": "^19.2.0",
155
162
  "@types/react-dom": "^19.2.0",
@@ -163,114 +170,6 @@ function renderPackageJson(name, version, testSetup) {
163
170
  function sortedByName(record) {
164
171
  return Object.fromEntries(Object.entries(record).sort(([a], [b]) => (a < b ? -1 : 1)));
165
172
  }
166
- function renderTsconfig() {
167
- return `${JSON.stringify({
168
- compilerOptions: {
169
- strict: true,
170
- noUncheckedIndexedAccess: true,
171
- forceConsistentCasingInFileNames: true,
172
- verbatimModuleSyntax: true,
173
- target: "ESNext",
174
- module: "ESNext",
175
- moduleResolution: "bundler",
176
- esModuleInterop: true,
177
- skipLibCheck: true,
178
- lib: ["ESNext", "DOM"],
179
- types: ["bun-types"],
180
- jsx: "react-jsx",
181
- noEmit: true,
182
- },
183
- include: ["bin", "src", "kumiko", "e2e", "playwright.config.ts"],
184
- }, null, 2)}\n`;
185
- }
186
- function renderBiomeJson() {
187
- return `${JSON.stringify({
188
- $schema: `https://biomejs.dev/schemas/${SCAFFOLD_BIOME_VERSION}/schema.json`,
189
- vcs: {
190
- enabled: true,
191
- clientKind: "git",
192
- useIgnoreFile: true,
193
- defaultBranch: "main",
194
- },
195
- files: {
196
- includes: [
197
- "src/**",
198
- "bin/**",
199
- "kumiko/**",
200
- "e2e/**",
201
- "playwright.config.ts",
202
- "!**/dist",
203
- "!kumiko/migrations",
204
- ],
205
- },
206
- formatter: {
207
- enabled: true,
208
- indentStyle: "space",
209
- indentWidth: 2,
210
- lineWidth: 100,
211
- lineEnding: "lf",
212
- },
213
- css: {
214
- parser: { cssModules: false, tailwindDirectives: true },
215
- },
216
- javascript: {
217
- formatter: {
218
- quoteStyle: "double",
219
- jsxQuoteStyle: "double",
220
- semicolons: "always",
221
- trailingCommas: "all",
222
- arrowParentheses: "always",
223
- },
224
- },
225
- json: { formatter: { indentWidth: 2, lineWidth: 80 } },
226
- linter: {
227
- enabled: true,
228
- rules: {
229
- preset: "recommended",
230
- correctness: { noUnusedVariables: "error", noUnusedImports: "error" },
231
- suspicious: { noExplicitAny: "error", noDebugger: "error", noConsole: "warn" },
232
- complexity: { useLiteralKeys: "off" },
233
- style: { useConst: "error" },
234
- nursery: { noFloatingPromises: "error" },
235
- },
236
- },
237
- overrides: [
238
- {
239
- includes: ["**/*.test.ts", "**/*.spec.ts", "**/*.integration.ts", "**/*.e2e.ts"],
240
- linter: {
241
- rules: {
242
- suspicious: { noConsole: "off" },
243
- style: { noNonNullAssertion: "off" },
244
- },
245
- },
246
- },
247
- ],
248
- }, null, 2)}\n`;
249
- }
250
- function renderBunfigToml() {
251
- return `[install]
252
- linker = "hoisted"
253
-
254
- [test]
255
- pathIgnorePatterns = [
256
- "**/e2e/**",
257
- "**/*.spec.ts",
258
- "**/dist/**",
259
- ]
260
- `;
261
- }
262
- function renderBunfigCiToml() {
263
- return `[install]
264
- linker = "hoisted"
265
-
266
- [test]
267
- pathIgnorePatterns = [
268
- "**/e2e/**",
269
- "**/*.spec.ts",
270
- "**/dist/**",
271
- ]
272
- `;
273
- }
274
173
  const FOUNDATION_FEATURES = [
275
174
  {
276
175
  name: "secrets",
@@ -389,45 +288,51 @@ ${passwordAndExtras}
389
288
  },
390
289
  },`;
391
290
  }
392
- function renderMain(appName) {
393
- const tenantId = deriveTenantId(appName);
291
+ // Ports match runDevApp's default and kumiko-testing's SCAFFOLD_E2E_PORT.
292
+ const SCAFFOLD_DEV_PORT = 4173;
293
+ const SCAFFOLD_E2E_PORT = 4174;
294
+ function renderAppConfig() {
394
295
  const imports = renderSortedImports([
395
- { specifier: "@cosmicdrift/kumiko-dev-server/env-schema", names: ["frameworkCoreEnvSchema"] },
396
- { specifier: "@cosmicdrift/kumiko-server-runtime", names: ["composeFeatures", "runProdApp"] },
397
- { specifier: "@cosmicdrift/kumiko-framework/engine", names: ["TenantId"], typeOnly: true },
398
- { specifier: "@cosmicdrift/kumiko-framework/env", names: ["composeEnvSchema"] },
296
+ { specifier: "@cosmicdrift/kumiko-framework/config", names: ["defineAppConfig"] },
399
297
  {
400
- specifier: "@cosmicdrift/kumiko-framework/crypto",
401
- names: ["requireKmsWiring", "resolveKmsWiring"],
298
+ specifier: "@cosmicdrift/kumiko-server-runtime/compose-features",
299
+ names: ["composeFeatures"],
402
300
  },
403
- { specifier: "zod", names: ["z"] },
404
- { specifier: "../src/run-config", names: ["APP_FEATURES", "HAS_AUTH"] },
301
+ { specifier: "./src/run-config", names: ["APP_FEATURES", "HAS_AUTH"] },
405
302
  ]);
406
- const authBlock = renderAdminAuthBlock(appName, [
407
- "// runProdApp validates envSchema before seeding: a missing or short value aborts the boot.",
408
- 'password: process.env["DEMO_ADMIN_PASSWORD"] ?? "",',
409
- 'displayName: "Admin",',
410
- ]);
411
- // The envSchema must cover the SAME features runProdApp mounts at boot:
412
- // `auth: { admin: … }` makes runProdApp auto-mix config/user/tenant/
413
- // auth-email-password, so the JWT_SECRET (min-32) declaration has to be part
414
- // of the boot-gate here too. Subject-key KMS is optional locally (plaintext +
415
- // loud warning) but required under NODE_ENV=production (fw#2317 / #2339).
416
- return `// Production-bootstrap. KUMIKO_DRY_RUN_ENV=boot exits after
417
- // composeFeatures + validateBoot + createRegistry without DB/Redis-connect
418
- // (see @cosmicdrift/kumiko-server-runtime runProdApp). The real dev boot
419
- // runs via \`bunx kumiko dev\` (in-repo dev-tool) with a Docker stack — DX-1.0
420
- // only covers the boot-mode path; \`kumiko dev\` lands in a later DX phase.
303
+ return `// The one place the tooling reads: features for schema, migrations and agent
304
+ // lint, client entries, and the ports this app claims (see \`kumiko ports\`).
305
+ // features is the composed list, so the bundled auth features count too.
421
306
 
422
307
  ${imports}
423
308
 
424
- const DEFAULT_TENANT_ID = "${tenantId}" as TenantId;
309
+ export default defineAppConfig({
310
+ features: composeFeatures([...APP_FEATURES], { includeBundled: HAS_AUTH }),
311
+ client: { entries: ["src/client.tsx"] },
312
+ ports: { dev: ${SCAFFOLD_DEV_PORT}, e2e: ${SCAFFOLD_E2E_PORT} },
313
+ });
314
+ `;
315
+ }
316
+ function renderEnvSchema(appName) {
317
+ const imports = renderSortedImports([
318
+ { specifier: "@cosmicdrift/kumiko-dev-server/env-schema", names: ["frameworkCoreEnvSchema"] },
319
+ { specifier: "@cosmicdrift/kumiko-framework/config", names: ["allAppFeatures"] },
320
+ { specifier: "@cosmicdrift/kumiko-framework/env", names: ["composeEnvSchema"] },
321
+ { specifier: "zod", names: ["z"] },
322
+ ]);
323
+ // The schema must cover the SAME features runProdApp mounts at boot:
324
+ // `auth: { admin: … }` makes runProdApp auto-mix config/user/tenant/
325
+ // auth-email-password, so their JWT_SECRET (min-32) declaration is part of the
326
+ // boot gate. kumiko.config.ts already holds that composed list.
327
+ return `// Env contract of the production processes (bin/main.ts, bin/kumiko.ts).
328
+ // Importing it must not read process.env; validation happens in runProdApp.
425
329
 
426
- const bootFeatures = composeFeatures(APP_FEATURES, { includeBundled: HAS_AUTH });
330
+ ${imports}
331
+ import appConfig from "../kumiko.config";
427
332
 
428
- const envSchema = composeEnvSchema({
333
+ export const envSchema = composeEnvSchema({
429
334
  core: frameworkCoreEnvSchema,
430
- features: bootFeatures,
335
+ features: allAppFeatures(appConfig),
431
336
  extend: z.object({
432
337
  DEMO_ADMIN_PASSWORD: z
433
338
  .string()
@@ -438,6 +343,34 @@ const envSchema = composeEnvSchema({
438
343
  .meta({ kumiko: { pulumi: { secret: true } } }),
439
344
  }),
440
345
  });
346
+ `;
347
+ }
348
+ function renderMain(appName) {
349
+ const tenantId = deriveTenantId(appName);
350
+ const imports = renderSortedImports([
351
+ { specifier: "@cosmicdrift/kumiko-server-runtime", names: ["runProdApp"] },
352
+ { specifier: "@cosmicdrift/kumiko-framework/engine", names: ["TenantId"], typeOnly: true },
353
+ {
354
+ specifier: "@cosmicdrift/kumiko-framework/crypto",
355
+ names: ["requireKmsWiring", "resolveKmsWiring"],
356
+ },
357
+ { specifier: "../src/run-config", names: ["APP_FEATURES"] },
358
+ { specifier: "./env-schema", names: ["envSchema"] },
359
+ ]);
360
+ const authBlock = renderAdminAuthBlock(appName, [
361
+ "// runProdApp validates envSchema before seeding: a missing or short value aborts the boot.",
362
+ 'password: process.env["DEMO_ADMIN_PASSWORD"] ?? "",',
363
+ 'displayName: "Admin",',
364
+ ]);
365
+ // Subject-key KMS is optional locally (plaintext + loud warning) but required
366
+ // under NODE_ENV=production (fw#2317 / #2339).
367
+ return `// Production-bootstrap. KUMIKO_DRY_RUN_ENV=boot exits after
368
+ // composeFeatures + validateBoot + createRegistry without DB/Redis-connect
369
+ // (see @cosmicdrift/kumiko-server-runtime runProdApp).
370
+
371
+ ${imports}
372
+
373
+ const DEFAULT_TENANT_ID = "${tenantId}" as TenantId;
441
374
 
442
375
  const kmsOpts = {
443
376
  logPrefix: "[${appName}]",
@@ -459,7 +392,7 @@ ${authBlock}
459
392
  });
460
393
  `;
461
394
  }
462
- function renderDev(appName) {
395
+ function renderServer(appName) {
463
396
  const tenantId = deriveTenantId(appName);
464
397
  const imports = renderSortedImports([
465
398
  { specifier: "@cosmicdrift/kumiko-dev-server", names: ["runDevApp"] },
@@ -474,7 +407,7 @@ function renderDev(appName) {
474
407
  'displayName: "Admin",',
475
408
  "emailVerified: true,",
476
409
  ]);
477
- return `// Dev bootstrap. \`bun --watch bin/dev.ts\` (see package.json scripts.dev)
410
+ return `// Dev bootstrap. \`bun --watch bin/server.ts\` (see package.json scripts.dev)
478
411
  // starts a full-featured dev server with auto-reload on code changes.
479
412
  // setupTestStack creates missing entity tables automatically — a new
480
413
  // r.entity(...) in a feature becomes a CREATE TABLE on the next reboot,
@@ -662,7 +595,9 @@ bun run boot
662
595
  \`\`\`
663
596
 
664
597
  Runs \`KUMIKO_DRY_RUN_ENV=boot bun bin/main.ts\` — validates feature composition
665
- + env schema, exits 0 without touching DB/Redis. Useful in CI. It needs
598
+ + env schema, exits 0 without touching DB/Redis. Useful in CI. It prints every
599
+ variable with its feature and status, never the value. An invalid value exits 1;
600
+ a missing one exits 0 without validating the wiring, so set
666
601
  \`DEMO_ADMIN_PASSWORD\` (min 12 chars, \`openssl rand -hex 24\`) in \`.env\`.
667
602
 
668
603
  Without \`PLATFORM_KEK\` / \`SUBJECT_KEYS_DATABASE_URL\` / \`KUMIKO_BLIND_INDEX_KEY\`
@@ -692,13 +627,23 @@ bun run schema:generate <name>
692
627
  \`deploy/Dockerfile\` + \`deploy/migrate-step.sh\` are scaffolded for container
693
628
  deploys. Build context = app repo root; migrations ship in \`kumiko/migrations/\`.
694
629
 
630
+ ## Managed files
631
+
632
+ Files whose first line reads \`kumiko-managed\` (deploy/, .github/workflows/,
633
+ .husky/pre-push, the bunfigs, kumiko/schema.ts) belong to the framework.
634
+ \`bun run check\` reports drift against the installed version;
635
+ \`bunx kumiko sync-managed --write\` restores them. To keep a local change, add
636
+ \`# kumiko-managed: local <reason>\` as line 2.
637
+
695
638
  ## Architecture
696
639
 
697
640
  - \`src/run-config.ts\` — single source of truth: which features your app mounts (\`APP_FEATURES\`, \`HAS_AUTH\`).
698
641
  - \`src/features/tasks/\` — demo feature (entity + handlers + screens + nav).
699
642
  - \`src/seed.ts\` — dev seed for demo tasks (\`bun dev\` only).
700
- - \`kumiko/schema.ts\` — same feature set → \`ENTITY_METAS\` for \`kumiko schema\`.
701
- - \`bin/dev.ts\` — dev-server entry (\`bun dev\`).
643
+ - \`kumiko.config.ts\` — the composed feature list, client entries and ports the tooling reads.
644
+ - \`kumiko/schema.ts\` — generated (\`kumiko-managed\`): \`ENTITY_METAS\` from kumiko.config.ts for \`kumiko schema\`.
645
+ - \`bin/server.ts\` — dev-server entry (\`bun dev\`).
646
+ - \`bin/env-schema.ts\` — env contract of the production processes.
702
647
  - \`bin/main.ts\` — production-bootstrap (\`bun run start\`).
703
648
  - \`bin/kumiko.ts\` — schema + consumer-ops CLI bundled into \`dist-server/kumiko.js\`.
704
649
  - \`docker-compose.yml\` — local Postgres + Redis for \`bun dev\`.
@@ -714,33 +659,14 @@ function renderStylesCss() {
714
659
  "",
715
660
  ].join("\n");
716
661
  }
717
- function renderKumikoSchema() {
718
- return [
719
- "// Live ENTITY_METAS source for `kumiko schema generate|apply|status`.",
720
- "//",
721
- "// Computes table-metas from the SAME composeFeatures(APP_FEATURES) the",
722
- "// runtime sees (runProdApp/runDevApp) — migration and runtime cannot drift.",
723
- "",
724
- 'import { collectTableMetas, type EntityTableMeta } from "@cosmicdrift/kumiko-framework/db";',
725
- 'import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";',
726
- 'import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";',
727
- 'import { APP_FEATURES, HAS_AUTH } from "../src/run-config";',
728
- "",
729
- "export const FEATURES: readonly FeatureDefinition[] = composeFeatures([...APP_FEATURES], {",
730
- " includeBundled: HAS_AUTH,",
731
- "});",
732
- "",
733
- "export const ENTITY_METAS: readonly EntityTableMeta[] = collectTableMetas(FEATURES);",
734
- "",
735
- ].join("\n");
736
- }
737
662
  // `kumiko check` fails a kind "app" without uiRoots, so a fresh app must declare them.
663
+ const SCAFFOLD_UI_ROOTS = ["src/features/*/web"];
738
664
  function renderKumikoManifest() {
739
665
  return `${JSON.stringify({
740
666
  kind: "app",
741
667
  sourceRoots: ["src", "bin"],
742
668
  testGlobs: ["src/**/*.{test,integration}.{ts,tsx}"],
743
- uiRoots: ["src/features/*/web"],
669
+ uiRoots: SCAFFOLD_UI_ROOTS,
744
670
  excludes: ["**/node_modules/**", "**/dist/**"],
745
671
  }, null, 2)}\n`;
746
672
  }
@@ -753,20 +679,20 @@ function renderBinKumiko() {
753
679
  "// consumer status|restart <name>` to recover a dead event consumer without",
754
680
  "// ad-hoc SQL. kumiko-build bundles this file to dist-server/kumiko.js.",
755
681
  "",
756
- 'import { frameworkCoreEnvSchema } from "@cosmicdrift/kumiko-dev-server/env-schema";',
682
+ 'import { allAppFeatures } from "@cosmicdrift/kumiko-framework/config";',
757
683
  'import { runConsumerCli } from "@cosmicdrift/kumiko-framework/consumer-cli";',
758
- 'import { composeEnvSchema, kmsSlotsOf } from "@cosmicdrift/kumiko-framework/env";',
684
+ 'import { kmsSlotsOf } from "@cosmicdrift/kumiko-framework/env";',
759
685
  'import { runSchemaCli } from "@cosmicdrift/kumiko-framework/schema-cli";',
760
- 'import { composeFeatures } from "@cosmicdrift/kumiko-server-runtime/compose-features";',
761
- 'import { APP_FEATURES, HAS_AUTH } from "../src/run-config";',
686
+ 'import appConfig from "../kumiko.config";',
687
+ 'import { envSchema } from "./env-schema";',
762
688
  "",
763
689
  "const [, , cmd, ...rest] = Bun.argv;",
764
690
  "// biome-ignore lint/suspicious/noConsole: CLI output is the feature.",
765
691
  "const out = { log: (l: string) => console.log(l), err: (l: string) => console.error(l) };",
766
692
  "",
767
693
  'if (cmd === "schema") {',
768
- " const features = composeFeatures([...APP_FEATURES], { includeBundled: HAS_AUTH });",
769
- " const kmsSlots = kmsSlotsOf(composeEnvSchema({ core: frameworkCoreEnvSchema, features }).schema);",
694
+ " const features = allAppFeatures(appConfig);",
695
+ " const kmsSlots = kmsSlotsOf(envSchema.schema);",
770
696
  " process.exit(",
771
697
  " await runSchemaCli(rest, process.env.INIT_CWD ?? process.cwd(), out, { features, kmsSlots }),",
772
698
  " );",
@@ -778,7 +704,7 @@ function renderBinKumiko() {
778
704
  "",
779
705
  "// biome-ignore lint/suspicious/noConsole: CLI output is the feature.",
780
706
  "console.error(",
781
- ` \`\\n Unknown: kumiko \${cmd ?? ""} — only 'kumiko schema <sub>' or 'kumiko consumer <sub>' in the standalone bundle.\\n\`,`,
707
+ ` \`\\n Unknown: kumiko \${cmd ?? ""} — the standalone bundle only runs 'kumiko schema' and 'kumiko consumer'.\\n\`,`,
782
708
  ");",
783
709
  "process.exit(1);",
784
710
  "",
@@ -789,7 +715,7 @@ async function instantiateScaffoldFeatures(features) {
789
715
  const effective = base.filter((f) => !COMPOSE_AUTO_MOUNTED_NAMES.has(f.name));
790
716
  const instances = [];
791
717
  for (const entry of effective) {
792
- const mod = (await import(entry.importPath));
718
+ const mod = (await import(__rewriteRelativeImportExtension(entry.importPath)));
793
719
  const exp = mod[entry.exportName];
794
720
  if (exp === undefined) {
795
721
  throw new Error(`scaffoldApp: ${entry.importPath} missing export ${entry.exportName} for ${entry.callExpression}`);
@@ -12,6 +12,9 @@ export type RenderDeployFilesOptions = {
12
12
  * `destination`. Lets the caller scaffold into one dir while detecting
13
13
  * optional surfaces in another (rare — mostly destination = sourceDir). */
14
14
  readonly sourceDir?: string;
15
+ /** Registry configs to treat as present although they may not exist yet:
16
+ * the managed-files sync writes bunfig.toml in the same pass as the Dockerfile. */
17
+ readonly ensuredRegistryConfigFiles?: readonly RegistryConfigFile[];
15
18
  };
16
19
  export type ScaffoldDeployOptions = RenderDeployFilesOptions & {
17
20
  /** Overwrite existing files instead of skipping them. */
@@ -85,5 +88,11 @@ export declare function scaffoldDeploy(options: ScaffoldDeployOptions): Scaffold
85
88
  /** Read-only: compares the rendered deploy files against what's on disk
86
89
  * without writing anything. */
87
90
  export declare function checkDeployDrift(options: RenderDeployFilesOptions): CheckDeployDriftResult;
91
+ export type KumikoReleaseTarget = {
92
+ readonly namespace: string;
93
+ readonly deployment: string;
94
+ };
95
+ /** Release target from `package.json#kumiko.deploy`; throws on an invalid deploy config. */
96
+ export declare function readReleaseTarget(sourceDir: string, appName: string): KumikoReleaseTarget;
88
97
  export declare function render(source: string, subs: Readonly<Record<string, string>>, flags: Readonly<Record<string, boolean>>): string;
89
98
  export {};
@@ -13,6 +13,7 @@
13
13
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
14
14
  import { dirname, join } from "node:path";
15
15
  import { fileURLToPath } from "node:url";
16
+ import { parseJsonOrThrow } from "@cosmicdrift/kumiko-framework/utils";
16
17
  import * as z from "zod";
17
18
  import { isKebabSegment } from "./kebab.js";
18
19
  const REGISTRY_CONFIG_FILES = ["bunfig.toml", ".npmrc"];
@@ -46,7 +47,7 @@ export function renderDeployFiles(options) {
46
47
  // a `seeds/` directory (e.g. studio) crash in Docker-build with
47
48
  // `failed to compute cache key: "/app/seeds": not found`.
48
49
  const sourceDir = options.sourceDir ?? destinationRoot;
49
- const detected = detectOptionalSurfaces(sourceDir, options.appName);
50
+ const detected = detectOptionalSurfaces(sourceDir, options.appName, options.ensuredRegistryConfigFiles ?? []);
50
51
  if (detected.hasPrivateGhPackages && detected.registryConfigFiles.length === 0) {
51
52
  // The Dockerfile only forwards the auth build-arg; without a registry
52
53
  // config scope entry bun resolves @cosmicdriftgamestudio/* against npmjs
@@ -143,10 +144,17 @@ const LOCAL_DEP_SPEC_RE = /^(file:|workspace:|link:)/;
143
144
  // full Postgres identifier grammar (no quoting support) since they are
144
145
  // interpolated into a shell string, not passed through a driver's escaping.
145
146
  const DB_IDENTIFIER_RE = /^[a-zA-Z0-9_][a-zA-Z0-9_-]{0,62}$/;
147
+ // Kubernetes object names (RFC 1123 label); they are interpolated into the
148
+ // generated release workflow, so nothing outside this charset may pass.
149
+ const K8S_NAME_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
146
150
  const kumikoDeployConfigSchema = z
147
151
  .object({
148
152
  dbUser: z.string().regex(DB_IDENTIFIER_RE).optional(),
149
153
  dbName: z.string().regex(DB_IDENTIFIER_RE).optional(),
154
+ /** Namespace of the prod deployment; default = appName. */
155
+ namespace: z.string().regex(K8S_NAME_RE).optional(),
156
+ /** Name of the prod Deployment; default = appName. */
157
+ deployment: z.string().regex(K8S_NAME_RE).optional(),
150
158
  // "directory" is the only behaviour left; the key stays valid so existing
151
159
  // package.json files keep scaffolding.
152
160
  stackNetwork: z
@@ -176,6 +184,25 @@ function parseDeployConfig(raw) {
176
184
  : ["kumiko", "deploy", ...issue.path.map(String)].join(".");
177
185
  throw new Error(`scaffoldDeploy: invalid package.json#${location} — ${issue?.message ?? "validation failed"}`);
178
186
  }
187
+ /** Release target from `package.json#kumiko.deploy`; throws on an invalid deploy config. */
188
+ export function readReleaseTarget(sourceDir, appName) {
189
+ const pkgJsonPath = join(sourceDir, "package.json");
190
+ const raw = existsSync(pkgJsonPath)
191
+ ? parseJsonOrThrow(readFileSync(pkgJsonPath, "utf-8"), pkgJsonPath)
192
+ : undefined;
193
+ const config = parseDeployConfig(extractDeployConfigRaw(raw));
194
+ const namespace = config.namespace ?? appName;
195
+ const deployment = config.deployment ?? appName;
196
+ for (const [field, value] of [
197
+ ["namespace", namespace],
198
+ ["deployment", deployment],
199
+ ]) {
200
+ if (!K8S_NAME_RE.test(value)) {
201
+ throw new Error(`scaffoldDeploy: "${value}" cannot serve as the release ${field} (must match ${K8S_NAME_RE}); set package.json#kumiko.deploy.${field}`);
202
+ }
203
+ }
204
+ return { namespace, deployment };
205
+ }
179
206
  function resolveDbIdentifier(configured, appName, field, role) {
180
207
  const effective = configured ?? appName;
181
208
  // The default (appName) already passed isKebabSegment's charset check but
@@ -197,7 +224,7 @@ function resolveDeployConfig(deployConfigRaw, appName) {
197
224
  dbName: resolveDbIdentifier(config.dbName, appName, "dbName", "DB name"),
198
225
  };
199
226
  }
200
- function detectOptionalSurfaces(sourceDir, appName) {
227
+ function detectOptionalSurfaces(sourceDir, appName, ensuredRegistryConfigFiles) {
201
228
  const hasSeeds = existsSync(join(sourceDir, "seeds"));
202
229
  let hasPrivateGhPackages = false;
203
230
  let installFromFullTree = false;
@@ -237,7 +264,7 @@ function detectOptionalSurfaces(sourceDir, appName) {
237
264
  console.warn(message);
238
265
  }
239
266
  }
240
- const registryConfigFiles = REGISTRY_CONFIG_FILES.filter((f) => existsSync(join(sourceDir, f)));
267
+ const registryConfigFiles = REGISTRY_CONFIG_FILES.filter((f) => ensuredRegistryConfigFiles.includes(f) || existsSync(join(sourceDir, f)));
241
268
  return {
242
269
  hasSeeds,
243
270
  hasPrivateGhPackages,
@@ -247,10 +274,11 @@ function detectOptionalSurfaces(sourceDir, appName) {
247
274
  };
248
275
  }
249
276
  // Unconsumed-mustache guard. After step 1+2 the only legitimate `{{`
250
- // survivors are Docker/Go template tags (`{{.Name}}`, leading `.`). An
277
+ // survivors are Docker/Go template tags (`{{.Name}}`, leading `.`) and GitHub
278
+ // Actions expressions (`${{ github.sha }}`, leading `$`). An
251
279
  // orphan close-tag (`{{/foo}}` without an opener) matches neither step and
252
280
  // would otherwise leak into the rendered file as a silent failure.
253
- const ORPHAN_MUSTACHE_RE = /\{\{(?!\.)[^}]*\}\}/;
281
+ const ORPHAN_MUSTACHE_RE = /(?<!\$)\{\{(?!\.)[^}]*\}\}/;
254
282
  export function render(source, subs, flags) {
255
283
  // Step 1: handle mustache-style block conditionals `{{#flag}}...{{/flag}}`
256
284
  // (multiline-aware via [\s\S]). When flag is truthy → keep inner content;
@@ -267,7 +295,7 @@ export function render(source, subs, flags) {
267
295
  // narrow: lowercase-leading identifier followed by alphanumerics. That
268
296
  // excludes Docker/Go template syntax like `{{.Name}}` (leading `.`)
269
297
  // which appears verbatim in the migrate-step.sh shell snippet.
270
- result = result.replace(/\{\{([a-z][a-zA-Z0-9]*)\}\}/g, (full, key) => {
298
+ result = result.replace(/(?<!\$)\{\{([a-z][a-zA-Z0-9]*)\}\}/g, (full, key) => {
271
299
  const value = subs[key];
272
300
  if (value === undefined) {
273
301
  throw new Error(`scaffoldDeploy.render: unknown placeholder "${full}"`);
@@ -127,6 +127,7 @@ function renderTsconfig() {
127
127
  target: "ESNext",
128
128
  module: "ESNext",
129
129
  moduleResolution: "bundler",
130
+ allowImportingTsExtensions: true,
130
131
  esModuleInterop: true,
131
132
  skipLibCheck: true,
132
133
  lib: ["ESNext"],