@cosmicdrift/kumiko-dev-server 0.368.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 +6 -1
  31. package/dist/run-dev-app.js +21 -3
  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
@@ -14,7 +14,7 @@
14
14
  import { AuthErrors, AuthHandlers, AuthQueries, } from "@cosmicdrift/kumiko-bundled-features/auth-email-password";
15
15
  import { seedAdminGuarded, } from "@cosmicdrift/kumiko-bundled-features/auth-email-password/seeding";
16
16
  import { EXT_SESSION_STORE, resolveSessionStore, resolveTokenVerifier, } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
17
- import { AUTH_MFA_FEATURE, AuthMfaHandlers, bindMfaRevokeAllOtherSessionsFromFeature, bindRevokeAllPatTokensFromFeature, } from "@cosmicdrift/kumiko-bundled-features/auth-mfa";
17
+ import { AUTH_MFA_FEATURE, AuthMfaHandlers, bindMfaRevokeAllOtherSessionsFromFeature, bindMfaTokenSecretsFromFeature, bindRevokeAllPatTokensFromFeature, } from "@cosmicdrift/kumiko-bundled-features/auth-mfa";
18
18
  import { buildEnvConfigOverrides, createConfigResolver, } from "@cosmicdrift/kumiko-bundled-features/config";
19
19
  import { bindPatAutoRevokeOnPasswordChangeFromFeature, PAT_FEATURE, patRateLimitFromFeature, revokeAllPatTokensForUser, } from "@cosmicdrift/kumiko-bundled-features/personal-access-tokens";
20
20
  import { bindAutoRevokeFromFeature, SESSIONS_FEATURE, } from "@cosmicdrift/kumiko-bundled-features/sessions";
@@ -23,6 +23,7 @@ import { createInMemoryLoginRateLimiter, parseTrustedProxyHopsEnv, TRUSTED_PROXY
23
23
  import { configureBlindIndexKey, configurePiiSubjectKms, } from "@cosmicdrift/kumiko-framework/crypto";
24
24
  import { configureEntityFieldEncryption } from "@cosmicdrift/kumiko-framework/db";
25
25
  import { collectWriteHandlerQns, createRegistry, FILE_STORAGE_PROVIDER_BOOT_SENTINEL, FILE_STORAGE_PROVIDER_ENV, findTierResolverUsage, requireTierResolverPlugin, validateAppCustomScreenWriteQns, validateBoot, } from "@cosmicdrift/kumiko-framework/engine";
26
+ import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
26
27
  import { warnIfNonUtcServerTimeZone } from "@cosmicdrift/kumiko-framework/time";
27
28
  import { applyBootSeeds } from "@cosmicdrift/kumiko-server-runtime/boot/apply-boot-seeds";
28
29
  import { resolveBootCrypto } from "@cosmicdrift/kumiko-server-runtime/boot/boot-crypto";
@@ -32,7 +33,7 @@ import { assertSessionBootInvariants } from "@cosmicdrift/kumiko-server-runtime/
32
33
  import { createKumikoServer, logInfo, } from "./create-kumiko-server.js";
33
34
  import { captureEphemeralDevPiiKms, resolveDevPiiKms } from "./resolve-dev-pii-kms.js";
34
35
  import { renderWelcomeBanner } from "./welcome-banner.js";
35
- import { addConfigAccessorFactory, buildBootExtraContext, loggerFromExtraContext, requireEnv, resolveAuthMail, } from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
36
+ import { addConfigAccessorFactory, buildBootExtraContext, loggerFromExtraContext, requireEnv, resolveAuthMail, resolveUnsubscribeWiring, } from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
36
37
  export async function runDevApp(options) {
37
38
  // Auto-mix Standard-Features im auth-mode via composeFeatures (single
38
39
  // source of truth — auch runProdApp und der per-app drizzle-Schema-
@@ -46,6 +47,16 @@ export async function runDevApp(options) {
46
47
  ? resolveAuthMail(options.auth, requireEnv("JWT_SECRET", envSource, "runDevApp"), envSource)
47
48
  : undefined;
48
49
  const composeAuthOptions = buildComposeAuthOptions(effectiveAuth);
50
+ const { unsubscribeLinks, unsubscribeRoutes } = resolveUnsubscribeWiring({
51
+ unsubscribe: options.unsubscribe,
52
+ publicOrigin: undefined,
53
+ mailBaseUrl: effectiveAuth?.mail?.baseUrl,
54
+ envSource,
55
+ warn: (message) => createFallbackLogger("runDevApp").warn(message),
56
+ });
57
+ const extraRoutes = unsubscribeRoutes.length === 0
58
+ ? options.extraRoutes
59
+ : [...(options.extraRoutes ?? []), ...unsubscribeRoutes];
49
60
  const features = composeFeatures(options.features, {
50
61
  includeBundled: !!effectiveAuth,
51
62
  ...(composeAuthOptions && { authOptions: composeAuthOptions }),
@@ -163,6 +174,7 @@ export async function runDevApp(options) {
163
174
  redis: deps.redis,
164
175
  crypto: bootCrypto,
165
176
  ...(bootLogger && { log: bootLogger }),
177
+ ...(unsubscribeLinks && { unsubscribeLinks }),
166
178
  });
167
179
  return {
168
180
  ...boot,
@@ -186,6 +198,12 @@ export async function runDevApp(options) {
186
198
  let tokenVerifier;
187
199
  const patFeature = features.find((f) => f.name === PAT_FEATURE);
188
200
  const mfaFeature = features.find((f) => f.name === AUTH_MFA_FEATURE);
201
+ // Same master secret as runProdApp. Without JWT_SECRET the feature keeps
202
+ // its explicit secrets, or its handlers report the missing binding.
203
+ const devMasterSecret = envSource["JWT_SECRET"];
204
+ if (mfaFeature && devMasterSecret) {
205
+ bindMfaTokenSecretsFromFeature(mfaFeature)?.(devMasterSecret, envSource);
206
+ }
189
207
  if (effectiveAuth &&
190
208
  mfaFeature &&
191
209
  registry?.getWriteHandler(AuthHandlers.switchTenantMfaGate) === undefined) {
@@ -239,7 +257,7 @@ export async function runDevApp(options) {
239
257
  ...(extraContext !== undefined && { extraContext }),
240
258
  ...(options.anonymousAccess !== undefined && { anonymousAccess: options.anonymousAccess }),
241
259
  ...(options.files !== undefined && { files: options.files }),
242
- ...(options.extraRoutes !== undefined && { extraRoutes: options.extraRoutes }),
260
+ ...(extraRoutes !== undefined && { extraRoutes }),
243
261
  ...(options.wire !== undefined && { wire: options.wire }),
244
262
  ...(trustedProxyHops !== undefined && { trustedProxyHops }),
245
263
  ...(finalEffectiveFeatures !== undefined && {
@@ -33,9 +33,9 @@ export type ScaffoldAppOptions = {
33
33
  * (the historical foundation). create-kumiko-app passes the picker output
34
34
  * here so the generated APP_FEATURES reflects the user's selection. */
35
35
  readonly features?: ReadonlyArray<ScaffoldFeatureEntry>;
36
- /** Test wiring (bunfigs, scripts, example tests, e2e) — `renderTestSetup` from
37
- * @cosmicdrift/kumiko-testing/scaffold. Without it the app gets the legacy
38
- * bunfig pair and no example tests. */
36
+ /** Test wiring (scripts, example tests, e2e) — `renderTestSetup` from
37
+ * @cosmicdrift/kumiko-testing/scaffold. Without it the app gets the managed
38
+ * bunfigs but no example tests and fewer standard scripts. */
39
39
  readonly testSetup?: ScaffoldTestSetupFactory;
40
40
  };
41
41
  export type ScaffoldAppResult = {
@@ -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 {};