@cosmicdrift/kumiko-dev-server 0.373.0 → 0.377.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 (34) hide show
  1. package/dist/compose-stacks.d.ts +3 -0
  2. package/dist/compose-stacks.js +11 -4
  3. package/dist/create-kumiko-server.d.ts +4 -2
  4. package/dist/create-kumiko-server.js +23 -25
  5. package/dist/managed-files/app-jobs.d.ts +4 -0
  6. package/dist/managed-files/app-jobs.js +68 -0
  7. package/dist/managed-files/check.d.ts +2 -0
  8. package/dist/managed-files/check.js +4 -2
  9. package/dist/managed-files/index.d.ts +2 -4
  10. package/dist/managed-files/index.js +2 -4
  11. package/dist/managed-files/registry.d.ts +7 -1
  12. package/dist/managed-files/registry.js +37 -5
  13. package/dist/managed-files/sync.js +10 -1
  14. package/dist/run-dev-app.d.ts +26 -10
  15. package/dist/run-dev-app.js +108 -37
  16. package/dist/scaffold-app-feature.js +1 -1
  17. package/dist/scaffold-app.d.ts +3 -0
  18. package/dist/scaffold-app.js +6 -7
  19. package/dist/scaffold-demo-tasks.js +1 -1
  20. package/dist/scaffold-deploy.d.ts +6 -0
  21. package/dist/scaffold-deploy.js +16 -1
  22. package/dist/testing.d.ts +17 -0
  23. package/dist/testing.js +46 -0
  24. package/package.json +19 -10
  25. package/src/changes.json +18 -0
  26. package/templates/managed/ci-public.yml.template +134 -0
  27. package/templates/managed/ci.yml.template +9 -6
  28. package/templates/managed/husky-pre-push.template +1 -7
  29. package/templates/managed/kumiko-schema.ts.template +16 -1
  30. package/templates/managed/release.yml.template +17 -1
  31. package/dist/managed-files/port-registry.d.ts +0 -17
  32. package/dist/managed-files/port-registry.js +0 -31
  33. package/dist/managed-files/static-app-config.d.ts +0 -16
  34. package/dist/managed-files/static-app-config.js +0 -99
@@ -110,9 +110,12 @@ export type SaasStackOptions<TCaps extends Readonly<Record<string, unknown>> = R
110
110
  readonly caps?: CreateCapOverviewOptions;
111
111
  /** Mounts waitlist; it needs auth-email-password and user-data-rights. */
112
112
  readonly waitlist?: WaitlistOptions;
113
+ /** Operator switches over the tiers: the stack then expects runProdApp / runDevApp's `featureToggles` option. */
114
+ readonly featureToggles?: boolean;
113
115
  };
114
116
  /** billing-foundation + tier-engine (+ caps, waitlist). `"stripe"` leaves
115
117
  * subscription-stripe and its webhook route to runProdApp's `billing` option;
118
+ * Without that option a stripe stack only warns at boot.
116
119
  * `"mollie"` mounts subscription-mollie, whose webhook route the app adds.
117
120
  * billing-foundation requires tenant-lifecycle and compliance-profiles, which
118
121
  * composeGdprStack({ tenantLifecycle: true }) mounts. */
@@ -40,8 +40,11 @@ import { createUserDataRightsDefaultsFeature } from "@cosmicdrift/kumiko-bundled
40
40
  import { createUserProfileFeature } from "@cosmicdrift/kumiko-bundled-features/user-profile";
41
41
  import { createWaitlistFeature, } from "@cosmicdrift/kumiko-bundled-features/waitlist";
42
42
  import { createRendererMailHtmlFeature, } from "@cosmicdrift/kumiko-renderer-mail-html";
43
- function withBootRequires(feature, stack, options) {
44
- return { ...feature, bootRequires: { stack, options } };
43
+ function withBootRequires(feature, stack, options, severity = "error") {
44
+ return {
45
+ ...feature,
46
+ bootRequires: { stack, options, ...(severity === "warn" && { severity }) },
47
+ };
45
48
  }
46
49
  export function stackFeatureNames(features) {
47
50
  return features.map((f) => f.name);
@@ -179,16 +182,20 @@ export function composeAuthStack(options = {}) {
179
182
  }
180
183
  /** billing-foundation + tier-engine (+ caps, waitlist). `"stripe"` leaves
181
184
  * subscription-stripe and its webhook route to runProdApp's `billing` option;
185
+ * Without that option a stripe stack only warns at boot.
182
186
  * `"mollie"` mounts subscription-mollie, whose webhook route the app adds.
183
187
  * billing-foundation requires tenant-lifecycle and compliance-profiles, which
184
188
  * composeGdprStack({ tenantLifecycle: true }) mounts. */
185
189
  export function composeSaasStack(options) {
186
190
  const foundation = createBillingFoundationFeature(options.billingFoundation ?? {});
191
+ const tierEngine = createTierEngineFeature(options.tiers);
187
192
  const out = [
188
193
  options.billing === "stripe"
189
- ? withBootRequires(foundation, "composeSaasStack", ["billing"])
194
+ ? withBootRequires(foundation, "composeSaasStack", ["billing"], "warn")
190
195
  : foundation,
191
- createTierEngineFeature(options.tiers),
196
+ options.featureToggles
197
+ ? withBootRequires(tierEngine, "composeSaasStack", ["featureToggles"])
198
+ : tierEngine,
192
199
  ];
193
200
  if (options.billing === "mollie")
194
201
  out.push(createSubscriptionMollieFeature(options.mollie));
@@ -2,7 +2,7 @@
2
2
  import { type AuthRoutesConfig, type ExtraRouteDefinition } from "@cosmicdrift/kumiko-framework/api";
3
3
  import { type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
4
4
  import type { ErrorDocsConfig } from "@cosmicdrift/kumiko-framework/errors";
5
- import { type PageHeadResolver, type PageHeadSystemQuery } from "@cosmicdrift/kumiko-headless/apex";
5
+ import { type PageHeadResolver, type PageHeadSystemQuery } from "@cosmicdrift/kumiko-renderer/headless/apex";
6
6
  import { type SystemWireDeps } from "@cosmicdrift/kumiko-server-runtime/extra-routes-deps";
7
7
  import { type TestStack, type TestStackOptions } from "@cosmicdrift/kumiko-testing/stack";
8
8
  type BunServer = typeof Bun extends undefined ? unknown : ReturnType<typeof Bun.serve>;
@@ -99,7 +99,7 @@ export type CreateKumikoServerOptions = {
99
99
  * whether the CLI keeps running with `--watch` afterwards.
100
100
  *
101
101
  * When `undefined` and a client entry is set, the app's own
102
- * src/styles.css wins, otherwise `@cosmicdrift/kumiko-renderer-web/styles.css`
102
+ * src/styles.css wins, otherwise `@cosmicdrift/kumiko-renderer/web/styles.css`
103
103
  * resolves via package exports, so no sample hardcodes a monorepo-relative path.
104
104
  *
105
105
  * `stylesheet: false` disables the CSS pipeline. */
@@ -183,6 +183,8 @@ export type CreateKumikoServerOptions = {
183
183
  * ships unchanged with status 200 — never a 500. `systemQuery` inside
184
184
  * the resolver runs as the anonymous role, same as prod. */
185
185
  readonly resolvePageHead?: PageHeadResolver;
186
+ /** Wraps the request handler before it reaches Bun.serve and `handle.fetch`, e.g. the demo read-only gate. */
187
+ readonly wrapFetch?: (handler: (req: Request, socketAddress?: string) => Promise<Response>) => (req: Request, socketAddress?: string) => Promise<Response>;
186
188
  };
187
189
  export type KumikoServerHandle = {
188
190
  /** The fetch handler that routes a Request through the dev-server
@@ -26,7 +26,7 @@ import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
26
26
  import { rankPreferredLocales } from "@cosmicdrift/kumiko-framework/i18n";
27
27
  import { createMetricsForFactory } from "@cosmicdrift/kumiko-framework/observability";
28
28
  import { TestUsers } from "@cosmicdrift/kumiko-framework/seeding";
29
- import { resolveAndInjectPageHead, } from "@cosmicdrift/kumiko-headless/apex";
29
+ import { resolveAndInjectPageHead, } from "@cosmicdrift/kumiko-renderer/headless/apex";
30
30
  import { startDevJobRunners } from "@cosmicdrift/kumiko-server-runtime/boot/job-run-logger";
31
31
  import { buildBunServeOptions, resolveDerivedMaxRequestBodySize, } from "@cosmicdrift/kumiko-server-runtime/bun-serve-options";
32
32
  import { makeDispatchSystemQuery, makeDispatchSystemWrite, } from "@cosmicdrift/kumiko-server-runtime/extra-routes-deps";
@@ -99,7 +99,7 @@ const RELOAD_SNIPPET = `
99
99
  // keyboard shrinks only the visual viewport, not the layout viewport that
100
100
  // `position: fixed` anchors to — a fixed bottom action bar would stay
101
101
  // pinned behind the keyboard instead of above it. iOS Safari ignores the
102
- // key; renderer-web's useKeyboardInset covers it via window.visualViewport.
102
+ // key; renderer/web's useKeyboardInset covers it via window.visualViewport.
103
103
  export const DEFAULT_HTML = `<!doctype html>
104
104
  <html lang="en">
105
105
  <head>
@@ -393,23 +393,20 @@ function expandWatchPatterns(patterns) {
393
393
  }
394
394
  return out;
395
395
  }
396
- // Resolve den Pfad zur Tailwind-Entry-CSS. Mehrere Fälle:
397
- // - Explicit string → den resolved'en absoluten Pfad verwenden
398
- // - false → CSS-Pipeline aus (undefined zurück)
399
- // - undefined + client(s):
400
- // 1. App-eigenes src/styles.css (App-Theme-Override) wenn vorhanden
401
- // 2. Sonst Default `@cosmicdrift/kumiko-renderer-web/styles.css` über Package-Exports
402
- // - undefined + kein clientEntry/clientEntries: undefined (keine CSS nötig)
396
+ // Resolves the path to the Tailwind entry CSS:
397
+ // - explicit string → the resolved absolute path
398
+ // - false → CSS pipeline off (undefined)
399
+ // - undefined + client entries:
400
+ // 1. the app's own src/styles.css (theme override) if present
401
+ // 2. else the default `@cosmicdrift/kumiko-renderer/web/styles.css` via package exports
402
+ // - undefined + no client entry: undefined (no CSS needed)
403
403
  //
404
- // Auto-Detection von src/styles.css spiegelt die Logik aus
405
- // build-prod-bundle:resolveStylesheetEntry — damit dev und prod identisch
406
- // resolven. Ohne diesen Check müsste jede App `stylesheet: "./src/styles.css"`
407
- // setzen, sonst greift in dev der renderer-web-Default und Brand-Tokens
408
- // werden ignoriert (DX-Falle).
404
+ // The src/styles.css auto-detection mirrors build-prod-bundle:resolveStylesheetEntry
405
+ // so dev and prod resolve identically. Without it every app would have to set
406
+ // `stylesheet: "./src/styles.css"`, or dev would fall back to the default and
407
+ // ignore the brand tokens.
409
408
  //
410
- // @internal — exportiert nur für Unit-Tests, nicht aus dem Package-Index
411
- // re-exportiert. Konsumenten gehen ausschließlich über die `stylesheet`-
412
- // Option der createKumikoServer-Aufrufstelle.
409
+ // @internal — exported for unit tests only, not re-exported from the package index.
413
410
  export function resolveStylesheet(options) {
414
411
  if (options.stylesheet === false)
415
412
  return undefined;
@@ -419,12 +416,12 @@ export function resolveStylesheet(options) {
419
416
  (options.clientEntries !== undefined && options.clientEntries.length > 0);
420
417
  if (!hasAnyEntry)
421
418
  return undefined;
422
- // App-eigenes src/styles.css schlägt den renderer-web-Default — gleiche
423
- // Logik wie kumiko-build, damit lokal/prod identisch bauen.
419
+ // The app's own src/styles.css beats the renderer/web default, same as
420
+ // kumiko-build, so local and prod builds match.
424
421
  const local = resolve(process.cwd(), "src/styles.css");
425
422
  if (existsSync(local))
426
423
  return local;
427
- // Bun.resolveSync folgt Package-Exports — "./styles.css" in renderer-web's
424
+ // Bun.resolveSync folgt Package-Exports — "./styles.css" in renderer/web's
428
425
  // package.json. Das Monorepo auflöst direkt auf den Workspace-File, eine
429
426
  // installierte Fremd-App auf den node_modules-File. Kein `fileURLToPath`
430
427
  // nötig, Bun gibt schon einen absoluten FS-Pfad zurück.
@@ -435,14 +432,14 @@ export function resolveStylesheet(options) {
435
432
  return undefined;
436
433
  }
437
434
  try {
438
- const resolved = Bun.resolveSync("@cosmicdrift/kumiko-renderer-web/styles.css", process.cwd());
435
+ const resolved = Bun.resolveSync("@cosmicdrift/kumiko-renderer/web/styles.css", process.cwd());
439
436
  if (!canResolveTailwindStylesheet(resolved, { bun: Bun, cwd: process.cwd() })) {
440
437
  return undefined;
441
438
  }
442
439
  return resolved;
443
440
  }
444
441
  catch (err) {
445
- logError("[kumiko-server] couldn't auto-resolve @cosmicdrift/kumiko-renderer-web/styles.css — " +
442
+ logError("[kumiko-server] couldn't auto-resolve @cosmicdrift/kumiko-renderer/web/styles.css — " +
446
443
  "pass `stylesheet: <path>` or `stylesheet: false` explicitly.", err);
447
444
  return undefined;
448
445
  }
@@ -556,7 +553,7 @@ export async function createKumikoServer(options) {
556
553
  // reloads after a bundle rebuild anyway.
557
554
  //
558
555
  // Without an explicit `stylesheet`, the default resolves through the
559
- // package exports of `@cosmicdrift/kumiko-renderer-web`, which works both
556
+ // package exports of `@cosmicdrift/kumiko-renderer/web`, which works both
560
557
  // for a workspace link and for an installed app (node_modules).
561
558
  let stylesheetPath;
562
559
  let killTailwind;
@@ -800,9 +797,9 @@ export async function createKumikoServer(options) {
800
797
  ? realpathSync(lexicalPublicDir)
801
798
  : lexicalPublicDir;
802
799
  // The app's cwd first, this package's own location as fallback (the cwd may
803
- // not have renderer-web installed, e.g. a fixture dir).
800
+ // not have renderer/web installed, e.g. a fixture dir).
804
801
  const rendererWebFontsDir = resolveRendererWebFontsDir([process.cwd(), import.meta.dir]);
805
- const handleFetch = async (req, socketAddress) => {
802
+ const routeFetch = async (req, socketAddress) => {
806
803
  const url = new URL(req.url);
807
804
  // Specific routes first — assets, reload-SSE, API.
808
805
  if (req.method === "GET") {
@@ -959,6 +956,7 @@ export async function createKumikoServer(options) {
959
956
  // header-hygiene note).
960
957
  return stripNoRouteMatchHeader(await stack.app.fetch(req, socketAddress));
961
958
  };
959
+ const handleFetch = options.wrapFetch ? options.wrapFetch(routeFetch) : routeFetch;
962
960
  // --- HTTP server (Bun only) ---
963
961
  // Under Node/vitest we skip Bun.serve entirely — the handle's
964
962
  // .fetch() is the test surface. Real dev runs under Bun, where
@@ -0,0 +1,4 @@
1
+ export declare function stripAppJobs(content: string): string;
2
+ export declare function appJobsOf(content: string): string;
3
+ /** Appends the app-owned jobs of `existing` to a freshly rendered workflow. */
4
+ export declare function withAppJobsOf(rendered: string, existing: string): string;
@@ -0,0 +1,68 @@
1
+ // Jobs named `app-<name>` in a managed workflow belong to the app: drift
2
+ // checks ignore them and a sync carries them over.
3
+ const JOBS_HEADER = /^jobs:\s*$/;
4
+ const JOB_START = /^ {2}([A-Za-z0-9_-]+):\s*$/;
5
+ const TOP_LEVEL_KEY = /^[^\s#]/;
6
+ const APP_JOB_PREFIX = "app-";
7
+ function isDetachedComment(line) {
8
+ return /^ {2}#/.test(line);
9
+ }
10
+ function findJobBlocks(lines) {
11
+ const header = lines.findIndex((line) => JOBS_HEADER.test(line));
12
+ if (header === -1)
13
+ return [];
14
+ const starts = [];
15
+ let jobsEnd = lines.length;
16
+ for (let index = header + 1; index < lines.length; index++) {
17
+ const line = lines[index] ?? "";
18
+ if (TOP_LEVEL_KEY.test(line)) {
19
+ jobsEnd = index;
20
+ break;
21
+ }
22
+ const match = JOB_START.exec(line);
23
+ if (match?.[1] !== undefined)
24
+ starts.push({ name: match[1], start: index });
25
+ }
26
+ return starts.map(({ name, start }, position) => {
27
+ let end = starts[position + 1]?.start ?? jobsEnd;
28
+ // Comments and blank lines right above the next job introduce that job.
29
+ while (end > start + 1 && (isDetachedComment(lines[end - 1] ?? "") || lines[end - 1] === "")) {
30
+ end--;
31
+ }
32
+ return { name, start: introStart(lines, start, starts[position - 1]), end };
33
+ });
34
+ }
35
+ function introStart(lines, start, previous) {
36
+ let first = start;
37
+ const floor = previous === undefined ? 0 : previous.start + 1;
38
+ while (first > floor && isDetachedComment(lines[first - 1] ?? ""))
39
+ first--;
40
+ return first;
41
+ }
42
+ function appJobBlocks(lines) {
43
+ return findJobBlocks(lines).filter((block) => block.name.startsWith(APP_JOB_PREFIX));
44
+ }
45
+ export function stripAppJobs(content) {
46
+ const lines = content.split("\n");
47
+ const removed = new Set();
48
+ for (const { start, end } of appJobBlocks(lines)) {
49
+ for (let index = start; index < end; index++)
50
+ removed.add(index);
51
+ if (lines[start - 1] === "")
52
+ removed.add(start - 1);
53
+ }
54
+ return lines.filter((_, index) => !removed.has(index)).join("\n");
55
+ }
56
+ export function appJobsOf(content) {
57
+ const lines = content.split("\n");
58
+ return appJobBlocks(lines)
59
+ .map(({ start, end }) => lines.slice(start, end).join("\n"))
60
+ .join("\n\n");
61
+ }
62
+ /** Appends the app-owned jobs of `existing` to a freshly rendered workflow. */
63
+ export function withAppJobsOf(rendered, existing) {
64
+ const appJobs = appJobsOf(existing);
65
+ if (appJobs === "")
66
+ return rendered;
67
+ return `${rendered.replace(/\n+$/, "")}\n\n${appJobs}\n`;
68
+ }
@@ -28,6 +28,8 @@ export type ManagedCheckResult = {
28
28
  /** kumiko.config.ts calls defineAppConfig: drift fails the check from then on. */
29
29
  readonly adopted: boolean;
30
30
  readonly files: readonly ManagedFileReport[];
31
+ /** Findings that do not fail the check. */
32
+ readonly warnings: readonly string[];
31
33
  };
32
34
  export declare function checkManagedFiles(facts: ManagedAppFacts): ManagedCheckResult;
33
35
  /** States that fail `kumiko check managed` once the app has adopted kumiko.config.ts. */
@@ -1,10 +1,11 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { mergeBunfig } from "@cosmicdrift/kumiko-testing/bunfig";
4
+ import { stripAppJobs } from "./app-jobs.js";
4
5
  import { parseJsonc } from "./jsonc.js";
5
6
  import { renderLineDiff } from "./line-diff.js";
6
7
  import { readManagedMarker, stripManagedMarker } from "./marker.js";
7
- import { BIOME_PRESET, listManagedFiles, renderManagedBunfig, TSCONFIG_PRESET, } from "./registry.js";
8
+ import { BIOME_PRESET, listManagedFiles, listManagedWarnings, renderManagedBunfig, TSCONFIG_PRESET, } from "./registry.js";
8
9
  function readIfExists(path) {
9
10
  return existsSync(path) ? readFileSync(path, "utf-8") : undefined;
10
11
  }
@@ -17,7 +18,7 @@ function checkTemplate(file, existing) {
17
18
  return { state: "invalid", message: marker.message };
18
19
  if (marker.kind === "local")
19
20
  return { state: "local", reason: marker.reason };
20
- const diff = compareBodies(stripManagedMarker(file.content), stripManagedMarker(existing));
21
+ const diff = compareBodies(stripAppJobs(stripManagedMarker(file.content)), stripAppJobs(stripManagedMarker(existing)));
21
22
  if (marker.kind === "unmarked")
22
23
  return diff === undefined ? { state: "unmarked" } : { state: "unmarked", diff };
23
24
  return diff === undefined ? { state: "ok" } : { state: "differs", diff };
@@ -101,6 +102,7 @@ export function checkManagedFiles(facts) {
101
102
  kind: file.kind,
102
103
  ...checkFile(file, facts),
103
104
  })),
105
+ warnings: listManagedWarnings(facts),
104
106
  };
105
107
  }
106
108
  /** States that fail `kumiko check managed` once the app has adopted kumiko.config.ts. */
@@ -1,8 +1,6 @@
1
- export { listAppPorts } from "@cosmicdrift/kumiko-framework/config";
1
+ export { findPortCollisions, listAppPorts, type PortClaim, type PortCollision, parseStaticAppConfig, readStaticAppConfig, type ScannedApp, type StaticAppConfig, scanWorkspaceApps, } from "@cosmicdrift/kumiko-guards/repo-manifest/app-ports";
2
2
  export { type AppScriptsReport, checkAppScripts, KNOWN_EXTRA_APP_SCRIPTS, PREPARE_SCRIPT, STANDARD_APP_SCRIPTS, type StandardAppScript, } from "./app-scripts.ts";
3
3
  export { checkManagedFiles, isManagedDrift, type ManagedCheckResult, type ManagedFileReport, type ManagedFileState, } from "./check.ts";
4
4
  export { addManagedMarker, MANAGED_MARKER, readManagedMarker, stripManagedMarker, } from "./marker.ts";
5
- export { findPortCollisions, type PortClaim, type PortCollision, type ScannedApp, scanWorkspaceApps, } from "./port-registry.ts";
6
- export { BIOME_PRESET, listManagedFiles, type ManagedAppFacts, type ManagedAppInput, type ManagedFile, type ManagedTemplateFile, renderBiomePresetJson, renderManagedBunfig, renderManagedTemplates, renderTsconfigPresetJson, resolveManagedAppFacts, TSCONFIG_PRESET, } from "./registry.ts";
7
- export { parseStaticAppConfig, readStaticAppConfig, type StaticAppConfig, } from "./static-app-config.ts";
5
+ export { BIOME_PRESET, listManagedFiles, listManagedWarnings, type ManagedAppFacts, type ManagedAppInput, type ManagedFile, type ManagedTemplateFile, renderBiomePresetJson, renderManagedBunfig, renderManagedTemplates, renderTsconfigPresetJson, resolveManagedAppFacts, TSCONFIG_PRESET, } from "./registry.ts";
8
6
  export { type SyncAction, type SyncManagedOptions, syncManagedFiles } from "./sync.ts";
@@ -1,8 +1,6 @@
1
- export { listAppPorts } from "@cosmicdrift/kumiko-framework/config";
1
+ export { findPortCollisions, listAppPorts, parseStaticAppConfig, readStaticAppConfig, scanWorkspaceApps, } from "@cosmicdrift/kumiko-guards/repo-manifest/app-ports";
2
2
  export { checkAppScripts, KNOWN_EXTRA_APP_SCRIPTS, PREPARE_SCRIPT, STANDARD_APP_SCRIPTS, } from "./app-scripts.js";
3
3
  export { checkManagedFiles, isManagedDrift, } from "./check.js";
4
4
  export { addManagedMarker, MANAGED_MARKER, readManagedMarker, stripManagedMarker, } from "./marker.js";
5
- export { findPortCollisions, scanWorkspaceApps, } from "./port-registry.js";
6
- export { BIOME_PRESET, listManagedFiles, renderBiomePresetJson, renderManagedBunfig, renderManagedTemplates, renderTsconfigPresetJson, resolveManagedAppFacts, TSCONFIG_PRESET, } from "./registry.js";
7
- export { parseStaticAppConfig, readStaticAppConfig, } from "./static-app-config.js";
5
+ export { BIOME_PRESET, listManagedFiles, listManagedWarnings, renderBiomePresetJson, renderManagedBunfig, renderManagedTemplates, renderTsconfigPresetJson, resolveManagedAppFacts, TSCONFIG_PRESET, } from "./registry.js";
8
6
  export { syncManagedFiles } from "./sync.js";
@@ -1,4 +1,5 @@
1
- import { type StaticAppConfig } from "./static-app-config.ts";
1
+ import { type StaticAppConfig } from "@cosmicdrift/kumiko-guards/repo-manifest/app-ports";
2
+ import { type KumikoReleaseTarget } from "../scaffold-deploy.ts";
2
3
  export declare const BIOME_PRESET = "@cosmicdrift/kumiko-framework/biome";
3
4
  export declare const TSCONFIG_PRESET = "@cosmicdrift/kumiko-framework/tsconfig.app.json";
4
5
  export type ManagedAppInput = {
@@ -13,7 +14,10 @@ export type ManagedAppFacts = {
13
14
  readonly appName: string;
14
15
  readonly hasUi: boolean;
15
16
  readonly config: StaticAppConfig;
17
+ readonly deploy: KumikoReleaseTarget;
16
18
  };
19
+ /** Ref used in the workflow templates while `kumiko.deploy.infraRef` is unset. */
20
+ export declare const DEFAULT_INFRA_REF = "main";
17
21
  /** Byte-compared after the marker lines are stripped. */
18
22
  export type ManagedTemplateFile = {
19
23
  readonly kind: "template";
@@ -40,6 +44,8 @@ export type ManagedSeedFile = {
40
44
  };
41
45
  export type ManagedFile = ManagedTemplateFile | ManagedBunfigFile | ManagedStructuralFile | ManagedSeedFile;
42
46
  export declare function resolveManagedAppFacts(input: ManagedAppInput): ManagedAppFacts;
47
+ /** Findings that do not fail `kumiko check managed`. */
48
+ export declare function listManagedWarnings(facts: ManagedAppFacts): readonly string[];
43
49
  export declare function renderManagedTemplates(facts: ManagedAppFacts): readonly ManagedTemplateFile[];
44
50
  export declare function managedBunfigFiles(facts: ManagedAppFacts): readonly ManagedBunfigFile[];
45
51
  /** Options are the app's choice; read them from the file so the check only flags real drift. */
@@ -5,13 +5,15 @@ import { existsSync, readFileSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { parseJsonOrThrow } from "@cosmicdrift/kumiko-framework/utils";
8
+ import { readStaticAppConfig, } from "@cosmicdrift/kumiko-guards/repo-manifest/app-ports";
8
9
  import { BUNFIG_FILES, renderBunfig } from "@cosmicdrift/kumiko-testing/bunfig";
9
10
  import { isKebabSegment } from "../kebab.js";
10
- import { readReleaseTarget, render, renderDeployFiles } from "../scaffold-deploy.js";
11
+ import { readReleaseTarget, render, renderDeployFiles, } from "../scaffold-deploy.js";
11
12
  import { addManagedMarker } from "./marker.js";
12
- import { readStaticAppConfig } from "./static-app-config.js";
13
13
  export const BIOME_PRESET = "@cosmicdrift/kumiko-framework/biome";
14
14
  export const TSCONFIG_PRESET = "@cosmicdrift/kumiko-framework/tsconfig.app.json";
15
+ /** Ref used in the workflow templates while `kumiko.deploy.infraRef` is unset. */
16
+ export const DEFAULT_INFRA_REF = "main";
15
17
  function templatesDir() {
16
18
  return join(dirname(fileURLToPath(import.meta.url)), "..", "..", "templates", "managed");
17
19
  }
@@ -39,8 +41,18 @@ export function resolveManagedAppFacts(input) {
39
41
  appName,
40
42
  hasUi: input.uiRoots.length > 0,
41
43
  config: readStaticAppConfig(input.appRoot),
44
+ deploy: readReleaseTarget(input.appRoot, appName),
42
45
  };
43
46
  }
47
+ /** Findings that do not fail `kumiko check managed`. */
48
+ export function listManagedWarnings(facts) {
49
+ if (facts.deploy.visibility === "private" && facts.deploy.infraRef === undefined) {
50
+ return [
51
+ `package.json#kumiko.deploy.infraRef is not set: ci.yml and release.yml call the infra workflows at @${DEFAULT_INFRA_REF}. Set it to an infra tag; a missing value will fail this check in a later release.`,
52
+ ];
53
+ }
54
+ return [];
55
+ }
44
56
  function marked(path, body, style, executable = false) {
45
57
  return { kind: "template", path, content: addManagedMarker(body, style), executable };
46
58
  }
@@ -48,7 +60,28 @@ function renderCiWorkflow(facts) {
48
60
  const infra = facts.config.kind === "defined" ? facts.config.ports.infra : undefined;
49
61
  const minio = infra?.s3 !== undefined;
50
62
  const meilisearch = infra?.meilisearch !== undefined;
51
- return render(readTemplate("ci.yml.template"), { appName: facts.appName, uiGuards: String(facts.hasUi) }, { minio, meilisearch, e2eInputs: minio || meilisearch });
63
+ if (facts.deploy.visibility === "public") {
64
+ return render(readTemplate("ci-public.yml.template"), { appName: facts.appName }, { uiGuards: facts.hasUi, meilisearch });
65
+ }
66
+ return render(readTemplate("ci.yml.template"), {
67
+ appName: facts.appName,
68
+ uiGuards: String(facts.hasUi),
69
+ infraRef: facts.deploy.infraRef ?? DEFAULT_INFRA_REF,
70
+ }, {
71
+ minio,
72
+ meilisearch,
73
+ e2eInputs: minio || meilisearch,
74
+ docStatusGuardOff: !facts.deploy.docStatusGuard,
75
+ });
76
+ }
77
+ function renderReleaseWorkflow(facts) {
78
+ const { namespace, deployment, visibility, infraRef } = facts.deploy;
79
+ return render(readTemplate("release.yml.template"), {
80
+ appName: facts.appName,
81
+ namespace,
82
+ deployment,
83
+ infraRef: infraRef ?? DEFAULT_INFRA_REF,
84
+ }, { privateDeploy: visibility === "private", publicRollout: visibility === "public" });
52
85
  }
53
86
  export function renderManagedTemplates(facts) {
54
87
  const deploy = renderDeployFiles({
@@ -56,12 +89,11 @@ export function renderManagedTemplates(facts) {
56
89
  destination: facts.appRoot,
57
90
  ensuredRegistryConfigFiles: [BUNFIG_FILES.unit],
58
91
  });
59
- const target = readReleaseTarget(facts.appRoot, facts.appName);
60
92
  return [
61
93
  ...deploy.files.map((file) => marked(`deploy/${file.output}`, file.content, "#", file.output.endsWith(".sh"))),
62
94
  marked(".husky/pre-push", readTemplate("husky-pre-push.template"), "#", true),
63
95
  marked(".github/workflows/cache-cleanup.yml", readTemplate("cache-cleanup.yml.template"), "#"),
64
- marked(".github/workflows/release.yml", render(readTemplate("release.yml.template"), { appName: facts.appName, ...target }, {}), "#"),
96
+ marked(".github/workflows/release.yml", renderReleaseWorkflow(facts), "#"),
65
97
  marked(".github/workflows/ci.yml", renderCiWorkflow(facts), "#"),
66
98
  marked("kumiko/schema.ts", readTemplate("kumiko-schema.ts.template"), "//"),
67
99
  ];
@@ -1,6 +1,7 @@
1
1
  import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join, relative, sep } from "node:path";
3
3
  import { mergeBunfig } from "@cosmicdrift/kumiko-testing/bunfig";
4
+ import { stripAppJobs, withAppJobsOf } from "./app-jobs.js";
4
5
  import { readManagedMarker } from "./marker.js";
5
6
  import { listManagedFiles, renderBiomePresetJson, renderManagedBunfig, renderTsconfigPresetJson, } from "./registry.js";
6
7
  // A symlinked .github or deploy/ would let the sync write outside the app.
@@ -24,10 +25,18 @@ function isBrokenSymlink(path) {
24
25
  return false;
25
26
  }
26
27
  }
28
+ // App jobs stay where the app put them as long as the rest matches the template.
29
+ function templateContent(rendered, existing) {
30
+ if (existing === undefined)
31
+ return rendered;
32
+ return stripAppJobs(existing) === stripAppJobs(rendered)
33
+ ? existing
34
+ : withAppJobsOf(rendered, existing);
35
+ }
27
36
  function plannedContent(file, existing) {
28
37
  switch (file.kind) {
29
38
  case "template":
30
- return { content: file.content, executable: file.executable };
39
+ return { content: templateContent(file.content, existing), executable: file.executable };
31
40
  case "bunfig": {
32
41
  const generated = renderManagedBunfig(file, existing);
33
42
  if (existing === undefined)
@@ -2,9 +2,11 @@ import { type SeedAdminOptions } from "@cosmicdrift/kumiko-bundled-features/auth
2
2
  import type { PostAuthLandingResolver } from "@cosmicdrift/kumiko-framework/api";
3
3
  import { type KmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
4
4
  import { type FeatureDefinition, type Registry, type ValidateBootOptions } from "@cosmicdrift/kumiko-framework/engine";
5
+ import { type ComposedEnvSchema } from "@cosmicdrift/kumiko-framework/env";
5
6
  import type { ErrorDocsConfig } from "@cosmicdrift/kumiko-framework/errors";
6
7
  import type { EnvelopeCipher, MasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
7
- import type { PageHeadResolver } from "@cosmicdrift/kumiko-headless/apex";
8
+ import type { PageHeadResolver } from "@cosmicdrift/kumiko-renderer/headless/apex";
9
+ import { type BillingBootOption, type DemoModeOption, type FileStorageBootOption, type KmsMode, type MailBootOption, type ProdSeedsConfig } from "@cosmicdrift/kumiko-server-runtime";
8
10
  import type { TestStack } from "@cosmicdrift/kumiko-testing/stack";
9
11
  import { type CreateKumikoServerOptions, type KumikoServerHandle } from "./create-kumiko-server.ts";
10
12
  export type { AccountLockoutSetup, AccountUnlockSetup, AuthMailOptions, EmailVerificationSetup, InviteSetup, PasswordResetSetup, SignupSetup, } from "@cosmicdrift/kumiko-server-runtime/run-prod-app";
@@ -65,6 +67,10 @@ export type RunDevAppAuthOptions = {
65
67
  /** Hook for app-specific seeding (demo data, fixtures). Runs after the
66
68
  * admin (when auth is active) in declared order. */
67
69
  export type SeedFn = (stack: TestStack) => Promise<void>;
70
+ /** Dev counterpart of `ProdSeedsConfig`: the declarative shortcuts plus free seeds that get the test stack. */
71
+ export type RunDevAppSeedsConfig = Omit<ProdSeedsConfig, "run"> & {
72
+ readonly run?: readonly SeedFn[];
73
+ };
68
74
  export type RunDevAppOptions = {
69
75
  /** App-spezifische Features. Im auth-mode werden config/user/tenant/
70
76
  * auth-email-password automatisch dazu gemischt — KEIN doppeltes
@@ -88,7 +94,7 @@ export type RunDevAppOptions = {
88
94
  * through `runDevApp` show the same resolved head prod does. See
89
95
  * `CreateKumikoServerOptions.resolvePageHead` for the full contract. */
90
96
  readonly resolvePageHead?: PageHeadResolver;
91
- /** CSS-Entry. Default: package-export `@cosmicdrift/kumiko-renderer-web/styles.css`
97
+ /** CSS-Entry. Default: package-export `@cosmicdrift/kumiko-renderer/web/styles.css`
92
98
  * wenn ein client-Entry gesetzt ist. `false` deaktiviert die CSS-Pipeline. */
93
99
  readonly stylesheet?: string | false;
94
100
  /** Tailwind `--watch` process on/off, see `CreateKumikoServerOptions.stylesheetWatch`. */
@@ -109,7 +115,7 @@ export type RunDevAppOptions = {
109
115
  readonly auth?: RunDevAppAuthOptions;
110
116
  /** Eigene Seed-Funktionen, laufen nach dem Admin (wenn auth) in
111
117
  * Array-Reihenfolge. */
112
- readonly seeds?: readonly SeedFn[];
118
+ readonly seeds?: readonly SeedFn[] | RunDevAppSeedsConfig;
113
119
  /** Extra-AppContext-Keys. `templateResolver` (immer) + `secrets` (wenn das
114
120
  * secrets-Feature gemountet ist) + `configResolver` (auth-mode) werden
115
121
  * automatisch ergänzt — App-Werte gewinnen. Symmetrisch zu runProdApp. */
@@ -154,13 +160,23 @@ export type RunDevAppOptions = {
154
160
  /** Hook for app-wired co-running components — symmetric to
155
161
  * runProdApp.wire (kumiko-framework#3050). */
156
162
  readonly wire?: CreateKumikoServerOptions["wire"];
157
- /** Feature-toggle resolver — durchgereicht an createKumikoServer →
158
- * setupTestStack. Sprint-8 Tier-Composition: per-Tenant unterschied-
159
- * liche features aktiv via globalFeatureToggleRuntime. Pattern in
160
- * bin/server.ts: createLateBoundHolder + post-boot runtime.initialize
161
- * in einem seed-fn, weil die runtime stack.db braucht und die seed-
162
- * Funktionen nach setupTestStack laufen. */
163
- readonly effectiveFeatures?: CreateKumikoServerOptions["effectiveFeatures"];
163
+ /** Same as `RunProdAppOptions["mail"]`: mounts `channel-email` and wires the auth mail flows. */
164
+ readonly mail?: MailBootOption;
165
+ /** Same as `RunProdAppOptions["fileStorage"]`. */
166
+ readonly fileStorage?: FileStorageBootOption;
167
+ /** Same as `RunProdAppOptions["kmsMode"]`; replaces the dev KMS default and cannot be combined
168
+ * with `kms` / `blindIndexKey`. */
169
+ readonly kmsMode?: KmsMode;
170
+ /** Same as `RunProdAppOptions["billing"]`. */
171
+ readonly billing?: BillingBootOption;
172
+ /** Same as `RunProdAppOptions["featureToggles"]`: global operator switches over the tier resolver. */
173
+ readonly featureToggles?: boolean;
174
+ /** Same as `RunProdAppOptions["demoMode"]`. */
175
+ readonly demoMode?: DemoModeOption;
176
+ /** Composed env schema (see `RunProdAppOptions["envSchema"]`). When set, the env is validated
177
+ * against it plus the blocks the boot options above need, and a violation throws a
178
+ * `KumikoBootError`. Dev does not run the dry-run modes. */
179
+ readonly envSchema?: ComposedEnvSchema;
164
180
  /** Print a first-run banner after the server starts (URL, admin login,
165
181
  * hot-reload hint, docs link). Default: off — apps that already log
166
182
  * their own startup shouldn't get double-printed. The scaffold template