@cosmicdrift/kumiko-dev-server 1.0.0 → 2.0.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 (66) hide show
  1. package/bin/kumiko-schema-check.ts +7 -0
  2. package/package.json +12 -7
  3. package/src/__tests__/build-prod-bundle.integration.test.ts +1 -1
  4. package/src/__tests__/build-server-bundle.test.ts +62 -0
  5. package/src/__tests__/compose-stacks.test.ts +271 -0
  6. package/src/__tests__/create-kumiko-server-errors.test.ts +54 -0
  7. package/src/__tests__/create-kumiko-server-options.test.ts +33 -0
  8. package/src/__tests__/create-kumiko-server.integration.test.ts +128 -1
  9. package/src/__tests__/discover-format.test.ts +1 -1
  10. package/src/__tests__/env-schema.integration.test.ts +1 -1
  11. package/src/__tests__/few-shot-corpus.test.ts +16 -11
  12. package/src/__tests__/merge-extra-context.test.ts +56 -0
  13. package/src/__tests__/resolve-stylesheet.test.ts +16 -0
  14. package/src/__tests__/saas-identity-wire.integration.test.ts +430 -0
  15. package/src/__tests__/scaffold-app-feature.test.ts +59 -6
  16. package/src/__tests__/scaffold-app.test.ts +34 -2
  17. package/src/__tests__/schema-apply.integration.test.ts +5 -2
  18. package/src/__tests__/setup-test-stack-from-features.integration.test.ts +30 -0
  19. package/src/__tests__/walkthrough.integration.test.ts +22 -6
  20. package/src/build-server-bundle.ts +33 -15
  21. package/src/build.ts +1 -2
  22. package/src/codegen/__tests__/render-codegen.test.ts +2 -1
  23. package/src/codegen/__tests__/run-codegen.test.ts +2 -2
  24. package/src/codegen/__tests__/strict-mode-diagnostics.test.ts +6 -1
  25. package/src/codegen/__tests__/watch.test.ts +1 -2
  26. package/src/codegen/render.ts +6 -1
  27. package/src/codegen/scan-events.ts +7 -5
  28. package/src/codegen/watch.ts +7 -4
  29. package/src/compose-stacks.ts +184 -0
  30. package/src/create-kumiko-server.ts +28 -5
  31. package/src/few-shot-corpus.ts +4 -3
  32. package/src/index.ts +30 -12
  33. package/src/run-dev-app.ts +195 -61
  34. package/src/scaffold-app-feature.ts +118 -15
  35. package/src/scaffold-app.ts +151 -79
  36. package/src/scaffold-demo-tasks.ts +233 -0
  37. package/src/schema-apply.ts +15 -2
  38. package/src/schema-check-core.ts +1 -1
  39. package/src/setup-test-stack-from-features.ts +61 -0
  40. package/src/welcome-banner.ts +1 -4
  41. package/src/__tests__/boot-extra-context.test.ts +0 -140
  42. package/src/__tests__/build-prod-bundle.test.ts +0 -311
  43. package/src/__tests__/cache-headers.test.ts +0 -83
  44. package/src/__tests__/compose-features-wiring.integration.test.ts +0 -382
  45. package/src/__tests__/compose-features.test.ts +0 -129
  46. package/src/__tests__/config-seed-boot.integration.test.ts +0 -158
  47. package/src/__tests__/inject-schema.test.ts +0 -62
  48. package/src/__tests__/renderer-web-css-relocation.integration.test.ts +0 -85
  49. package/src/__tests__/renderer-web-shell-sentinel.test.ts +0 -35
  50. package/src/__tests__/require-env.test.ts +0 -29
  51. package/src/__tests__/resolve-auth-mail.test.ts +0 -69
  52. package/src/__tests__/resolve-tailwind-cli.test.ts +0 -81
  53. package/src/__tests__/run-prod-app-env-source.test.ts +0 -157
  54. package/src/__tests__/run-prod-app-spec.test.ts +0 -57
  55. package/src/__tests__/run-prod-app.integration.test.ts +0 -840
  56. package/src/__tests__/session-wiring.test.ts +0 -51
  57. package/src/__tests__/try-hono-first.test.ts +0 -63
  58. package/src/boot/apply-boot-seeds.ts +0 -18
  59. package/src/build-prod-bundle.ts +0 -697
  60. package/src/compose-features.ts +0 -145
  61. package/src/extra-routes-deps.ts +0 -47
  62. package/src/inject-schema.ts +0 -24
  63. package/src/resolve-tailwind-cli.ts +0 -45
  64. package/src/run-prod-app.ts +0 -1492
  65. package/src/session-wiring.ts +0 -29
  66. package/src/try-hono-first.ts +0 -46
@@ -0,0 +1,184 @@
1
+ // Composable SaaS stack presets — explicit opt-in blocks for run-config.ts.
2
+ // Presets export feature instances only; no implicit PAT scopes, anonymousAccess,
3
+ // or tier maps (those stay in bin/server.ts / app run-config).
4
+
5
+ import { createAuditFeature } from "@cosmicdrift/kumiko-bundled-features/audit";
6
+ import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
7
+ import {
8
+ type AuthMfaFeatureOptions,
9
+ createAuthMfaFeature,
10
+ } from "@cosmicdrift/kumiko-bundled-features/auth-mfa";
11
+ import { createComplianceProfilesFeature } from "@cosmicdrift/kumiko-bundled-features/compliance-profiles";
12
+ import { createDataRetentionFeature } from "@cosmicdrift/kumiko-bundled-features/data-retention";
13
+ import { createDeliveryFeature } from "@cosmicdrift/kumiko-bundled-features/delivery";
14
+ import { fileFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/file-foundation";
15
+ import { fileProviderInMemoryFeature } from "@cosmicdrift/kumiko-bundled-features/file-provider-inmemory";
16
+ import { fileProviderS3Feature } from "@cosmicdrift/kumiko-bundled-features/file-provider-s3";
17
+ import { fileProviderS3EnvFeature } from "@cosmicdrift/kumiko-bundled-features/file-provider-s3-env";
18
+ import { createFilesFeature } from "@cosmicdrift/kumiko-bundled-features/files";
19
+ import { createJobsFeature } from "@cosmicdrift/kumiko-bundled-features/jobs";
20
+ import {
21
+ createLegalPagesFeature,
22
+ type LegalPagesOptions,
23
+ } from "@cosmicdrift/kumiko-bundled-features/legal-pages";
24
+ import { mailFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/mail-foundation";
25
+ import { mailTransportInMemoryFeature } from "@cosmicdrift/kumiko-bundled-features/mail-transport-inmemory";
26
+ import { mailTransportSmtpFeature } from "@cosmicdrift/kumiko-bundled-features/mail-transport-smtp";
27
+ import { createRateLimitingFeature } from "@cosmicdrift/kumiko-bundled-features/rate-limiting";
28
+ import { createRendererFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/renderer-foundation";
29
+ import { createRendererSimpleFeature } from "@cosmicdrift/kumiko-bundled-features/renderer-simple";
30
+ import { createSessionsFeature } from "@cosmicdrift/kumiko-bundled-features/sessions";
31
+ import { createTemplateResolverFeature } from "@cosmicdrift/kumiko-bundled-features/template-resolver";
32
+ import { createTenantLifecycleFeature } from "@cosmicdrift/kumiko-bundled-features/tenant-lifecycle";
33
+ import { createTextContentFeature } from "@cosmicdrift/kumiko-bundled-features/text-content";
34
+ import {
35
+ createUserDataRightsFeature,
36
+ type UserDataRightsOptions,
37
+ } from "@cosmicdrift/kumiko-bundled-features/user-data-rights";
38
+ import { createUserDataRightsDefaultsFeature } from "@cosmicdrift/kumiko-bundled-features/user-data-rights-defaults";
39
+ import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
40
+
41
+ export type FileProviderKind = "inmemory" | "s3" | "s3-env";
42
+ export type MailTransportKind = "inmemory" | "smtp";
43
+ export type GdprStackOrder = "retention-first" | "compliance-first";
44
+
45
+ export type FileStackOptions = {
46
+ readonly providers: readonly FileProviderKind[];
47
+ readonly includeFilesFeature?: boolean;
48
+ };
49
+
50
+ export type MailStackOptions = {
51
+ readonly transports: readonly MailTransportKind[];
52
+ };
53
+
54
+ export type PagesStackOptions = {
55
+ readonly wrapLayout?: LegalPagesOptions["wrapLayout"];
56
+ };
57
+
58
+ export type GdprStackOptions = {
59
+ readonly order?: GdprStackOrder;
60
+ readonly sessions?: boolean;
61
+ readonly tenantLifecycle?: boolean;
62
+ /** Optional tokenVerifier provider(s) (e.g. PAT) when `sessions` is on.
63
+ * Session-only stacks need none — sessions registers sessionStore, which
64
+ * satisfies auth-foundation's boot check (#1570). Pass providers when the
65
+ * app also wants Bearer/PAT auth. */
66
+ readonly providers?: readonly FeatureDefinition[];
67
+ };
68
+
69
+ export type UserDataRightsStackOptions = {
70
+ readonly userDataRights?: UserDataRightsOptions;
71
+ readonly includeDefaults?: boolean;
72
+ };
73
+
74
+ // `sessions` may live on composeIdentityStack OR composeGdprStack — never both
75
+ // with sessions:true. composeOpsStack used to accept it too; combining presets
76
+ // that each push "sessions" crashes createRegistry with "duplicate feature".
77
+ export type OpsStackOptions = {
78
+ readonly delivery?: boolean;
79
+ readonly audit?: boolean;
80
+ readonly jobs?: boolean;
81
+ readonly rateLimiting?: boolean;
82
+ };
83
+
84
+ /** sessions (+ optional auth-mfa). config/user/tenant/auth-email-password stay
85
+ * on composeFeatures(includeBundled). Pass `mfa` options to mount TOTP.
86
+ * When `sessions` is on (the default), auth-foundation is mounted alongside it —
87
+ * the framework's registry now hard-requires it (sessions.requires("auth-foundation")).
88
+ * Session-only is enough for boot (#1570); optional Bearer/PAT still needs a
89
+ * tokenVerifier provider from the caller (e.g. createPersonalAccessTokensFeature
90
+ * ({ scopes })) — PAT scopes are app-specific, so they stay caller-owned. */
91
+ export type IdentityStackOptions = {
92
+ readonly sessions?: boolean;
93
+ readonly mfa?: AuthMfaFeatureOptions;
94
+ };
95
+
96
+ export function stackFeatureNames(features: readonly FeatureDefinition[]): string[] {
97
+ return features.map((f) => f.name);
98
+ }
99
+
100
+ export function composePagesStack(options: PagesStackOptions = {}): FeatureDefinition[] {
101
+ return [
102
+ createTextContentFeature(),
103
+ createLegalPagesFeature(
104
+ options.wrapLayout !== undefined ? { wrapLayout: options.wrapLayout } : {},
105
+ ),
106
+ ];
107
+ }
108
+
109
+ export function composeRendererStack(): FeatureDefinition[] {
110
+ return [
111
+ createTemplateResolverFeature(),
112
+ createRendererFoundationFeature(),
113
+ createRendererSimpleFeature(),
114
+ ];
115
+ }
116
+
117
+ export function composeMailStack(options: MailStackOptions): FeatureDefinition[] {
118
+ const out: FeatureDefinition[] = [mailFoundationFeature];
119
+ for (const transport of options.transports) {
120
+ if (transport === "inmemory") out.push(mailTransportInMemoryFeature);
121
+ if (transport === "smtp") out.push(mailTransportSmtpFeature);
122
+ }
123
+ return out;
124
+ }
125
+
126
+ export function composeFileStack(options: FileStackOptions): FeatureDefinition[] {
127
+ const out: FeatureDefinition[] = [fileFoundationFeature];
128
+ for (const provider of options.providers) {
129
+ if (provider === "inmemory") out.push(fileProviderInMemoryFeature);
130
+ if (provider === "s3") out.push(fileProviderS3Feature);
131
+ if (provider === "s3-env") out.push(fileProviderS3EnvFeature);
132
+ }
133
+ if (options.includeFilesFeature ?? true) out.push(createFilesFeature());
134
+ return out;
135
+ }
136
+
137
+ /** Retention + compliance (+ optional lifecycle/sessions). UDR/files mount separately — order varies per app. */
138
+ export function composeGdprStack(options: GdprStackOptions = {}): FeatureDefinition[] {
139
+ const order = options.order ?? "retention-first";
140
+ const retention = createDataRetentionFeature();
141
+ const compliance = createComplianceProfilesFeature();
142
+ const out: FeatureDefinition[] =
143
+ order === "compliance-first" ? [compliance, retention] : [retention, compliance];
144
+ if (options.tenantLifecycle) out.push(createTenantLifecycleFeature());
145
+ if (options.sessions) {
146
+ out.push(authFoundationFeature, createSessionsFeature());
147
+ if (options.providers) out.push(...options.providers);
148
+ }
149
+ return out;
150
+ }
151
+
152
+ export function composeUserDataRightsStack(
153
+ options: UserDataRightsStackOptions = {},
154
+ ): FeatureDefinition[] {
155
+ const includeDefaults = options.includeDefaults ?? true;
156
+ const out: FeatureDefinition[] = [createUserDataRightsFeature(options.userDataRights ?? {})];
157
+ if (includeDefaults) out.push(createUserDataRightsDefaultsFeature());
158
+ return out;
159
+ }
160
+
161
+ export function composeOpsStack(options: OpsStackOptions = {}): FeatureDefinition[] {
162
+ const delivery = options.delivery ?? true;
163
+ const audit = options.audit ?? true;
164
+ const jobs = options.jobs ?? true;
165
+ const rateLimiting = options.rateLimiting ?? false;
166
+ const out: FeatureDefinition[] = [];
167
+ if (delivery) out.push(createDeliveryFeature());
168
+ if (audit) out.push(createAuditFeature());
169
+ if (jobs) out.push(createJobsFeature());
170
+ if (rateLimiting) out.push(createRateLimitingFeature());
171
+ return out;
172
+ }
173
+
174
+ /** Identity opt-ins: sessions by default; auth-mfa when `mfa` options given.
175
+ * Do not also pass `sessions: true` to composeGdprStack — duplicate feature. */
176
+ export function composeIdentityStack(options: IdentityStackOptions = {}): FeatureDefinition[] {
177
+ const sessions = options.sessions ?? true;
178
+ const out: FeatureDefinition[] = [];
179
+ if (sessions) {
180
+ out.push(authFoundationFeature, createSessionsFeature());
181
+ }
182
+ if (options.mfa !== undefined) out.push(createAuthMfaFeature(options.mfa));
183
+ return out;
184
+ }
@@ -19,6 +19,7 @@ import { existsSync, mkdtempSync, statSync } from "node:fs";
19
19
  import { readFile, watch } from "node:fs/promises";
20
20
  import { tmpdir } from "node:os";
21
21
  import { join, resolve } from "node:path";
22
+ import { resolveAnonymousAccessFromRegistry } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
22
23
  import { type AuthRoutesConfig, generateToken } from "@cosmicdrift/kumiko-framework/api";
23
24
  import { buildAppSchema, type FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
24
25
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
@@ -29,11 +30,18 @@ import {
29
30
  type TestStackOptions,
30
31
  TestUsers,
31
32
  } from "@cosmicdrift/kumiko-framework/stack";
32
- import { type ExtraRoutesSystemDeps, makeDispatchSystemWrite } from "./extra-routes-deps";
33
- import { injectSchema } from "./inject-schema";
34
- import { canResolveTailwindStylesheet, resolveTailwindCli } from "./resolve-tailwind-cli";
35
- import { buildBunServeOptions } from "./run-prod-app";
36
- import { tryHonoFirst } from "./try-hono-first";
33
+ import { startDevJobRunners } from "@cosmicdrift/kumiko-server-runtime/boot/job-run-logger";
34
+ import { buildBunServeOptions } from "@cosmicdrift/kumiko-server-runtime/bun-serve-options";
35
+ import {
36
+ type ExtraRoutesSystemDeps,
37
+ makeDispatchSystemWrite,
38
+ } from "@cosmicdrift/kumiko-server-runtime/extra-routes-deps";
39
+ import { injectSchema } from "@cosmicdrift/kumiko-server-runtime/inject-schema";
40
+ import {
41
+ canResolveTailwindStylesheet,
42
+ resolveTailwindCli,
43
+ } from "@cosmicdrift/kumiko-server-runtime/resolve-tailwind-cli";
44
+ import { tryHonoFirst } from "@cosmicdrift/kumiko-server-runtime/try-hono-first";
37
45
 
38
46
  // Runtime-detection. The dev-server is meant to run under Bun (Kumiko's
39
47
  // target runtime), but the test-suite runs under vitest on Node — we
@@ -335,6 +343,7 @@ async function watchDir(
335
343
  } catch (err) {
336
344
  // signal.abort() wirft AbortError aus dem async-iterator; das ist
337
345
  // gewollt und kein Fehler. Andere Errors weiterreichen.
346
+ // skip: AbortSignal fired the abort, this is expected teardown not a real error
338
347
  if ((err as { name?: string }).name === "AbortError") return;
339
348
  throw err;
340
349
  }
@@ -665,10 +674,14 @@ export async function createKumikoServer(
665
674
  ...(options.auth !== undefined && { authConfig: options.auth }),
666
675
  ...(options.extraContext !== undefined && { extraContext: options.extraContext }),
667
676
  ...(options.anonymousAccess !== undefined && { anonymousAccess: options.anonymousAccess }),
677
+ enrichAnonymousAccess: (base, deps) => resolveAnonymousAccessFromRegistry(base, deps),
668
678
  ...(options.files !== undefined && { files: options.files }),
669
679
  ...(options.effectiveFeatures !== undefined && {
670
680
  effectiveFeatures: options.effectiveFeatures,
671
681
  }),
682
+ // jobs: {} = enqueuer-only; startDevJobRunners below is the sole
683
+ // consumer/cron-scheduler per lane, so runOnBoot/cron jobs don't double-fire.
684
+ jobs: {},
672
685
  });
673
686
  await createEventsTable(stack.db);
674
687
  await pushEntityProjectionTables(stack, stack.registry);
@@ -707,6 +720,14 @@ export async function createKumikoServer(
707
720
  await stack.eventDispatcher.start();
708
721
  }
709
722
 
723
+ const redisUrl = `redis://${stack.redis.redis.options.host}:${stack.redis.redis.options.port}/${stack.redis.redis.options.db}`;
724
+ const devJobRunners = await startDevJobRunners({
725
+ registry: stack.registry,
726
+ db: stack.db,
727
+ context: { db: stack.db, registry: stack.registry },
728
+ redisUrl,
729
+ });
730
+
710
731
  // Dev user = TestUsers.admin. Demo features are openToAll but the
711
732
  // auth-middleware still needs a valid JWT to let the request past.
712
733
  // Nicht genutzt wenn `options.auth` gesetzt ist — dann macht der Client
@@ -936,6 +957,7 @@ export async function createKumikoServer(
936
957
  dir,
937
958
  async (filename) => {
938
959
  const action = classifyChange(filename);
960
+ // skip: file change classified as ignore (test/css/json), nothing to rebuild
939
961
  if (action === "ignore") return;
940
962
  if (action === "restart") {
941
963
  logInfo(
@@ -977,6 +999,7 @@ export async function createKumikoServer(
977
999
  if (stack.eventDispatcher) {
978
1000
  await stack.eventDispatcher.stop();
979
1001
  }
1002
+ await devJobRunners.stop();
980
1003
  await stack.cleanup();
981
1004
  };
982
1005
 
@@ -52,7 +52,7 @@ export type CorpusWarning = {
52
52
  /** Human-readable explanation. Currently only "parser-throw" but
53
53
  * kept open-ended so future builders can add more (e.g.
54
54
  * "duplicate-id", "no-feature-name"). */
55
- readonly reason: string;
55
+ readonly message: string;
56
56
  };
57
57
 
58
58
  export type FewShotEntry = {
@@ -161,7 +161,7 @@ export function buildFewShotCorpus(options: BuildFewShotCorpusOptions): FewShotC
161
161
  // warning, drop the second occurrence.
162
162
  warnings.push({
163
163
  sourcePath: entry.sourcePath,
164
- reason: `duplicate-id: collides with ${previousPath}`,
164
+ message: `duplicate-id: collides with ${previousPath}`,
165
165
  });
166
166
  continue;
167
167
  }
@@ -198,7 +198,7 @@ function buildEntry(filePath: string, repoRoot: string): BuildEntryResult {
198
198
  // hide newly broken feature-files until L2 hit them.
199
199
  const detail = err instanceof Error ? err.message : String(err);
200
200
  return {
201
- warning: { sourcePath, reason: `parser-throw: ${detail}` },
201
+ warning: { sourcePath, message: `parser-throw: ${detail}` },
202
202
  };
203
203
  }
204
204
 
@@ -295,6 +295,7 @@ function walkDir(dir: string, acc: string[]): void {
295
295
  try {
296
296
  entries = readdirSync(dir, { withFileTypes: true });
297
297
  } catch {
298
+ // skip: directory missing/unreadable, nothing to walk
298
299
  return;
299
300
  }
300
301
  for (const entry of entries) {
package/src/index.ts CHANGED
@@ -13,6 +13,10 @@
13
13
  // Login-Routes + Error-Map, ruft seedAdmin im onAfterSetup. Default
14
14
  // für Sample-Apps und Showcases — 5-10 Zeilen Bootstrap statt 50.
15
15
 
16
+ export {
17
+ type ComposeFeaturesOptions,
18
+ composeFeatures,
19
+ } from "@cosmicdrift/kumiko-server-runtime/compose-features";
16
20
  // Build-Toolchain (buildProdBundle, Bun.build, Tailwind-Pipeline, ts-morph) lebt
17
21
  // im Sub-Path-Export `@cosmicdrift/kumiko-dev-server/build`. Damit zieht der Main-Barrel
18
22
  // kein Bun-Toolchain-Bundle mehr in Production-Reads (z.B. wenn drizzle-kit
@@ -25,7 +29,27 @@ export {
25
29
  type ScanWarning,
26
30
  scanEvents,
27
31
  } from "./codegen";
28
- export { type ComposeFeaturesOptions, composeFeatures } from "./compose-features";
32
+ export {
33
+ composeFileStack,
34
+ composeGdprStack,
35
+ composeIdentityStack,
36
+ composeMailStack,
37
+ composeOpsStack,
38
+ composePagesStack,
39
+ composeRendererStack,
40
+ composeUserDataRightsStack,
41
+ type FileProviderKind,
42
+ type FileStackOptions,
43
+ type GdprStackOptions,
44
+ type GdprStackOrder,
45
+ type IdentityStackOptions,
46
+ type MailStackOptions,
47
+ type MailTransportKind,
48
+ type OpsStackOptions,
49
+ type PagesStackOptions,
50
+ stackFeatureNames,
51
+ type UserDataRightsStackOptions,
52
+ } from "./compose-stacks";
29
53
  export {
30
54
  type CreateKumikoServerOptions,
31
55
  createKumikoServer,
@@ -43,17 +67,6 @@ export type {
43
67
  export { buildFewShotCorpus, pathToId } from "./few-shot-corpus";
44
68
  export type { RunDevAppAuthOptions, RunDevAppOptions, SeedFn } from "./run-dev-app";
45
69
  export { runDevApp } from "./run-dev-app";
46
- export type {
47
- EmailVerificationSetup,
48
- InviteSetup,
49
- PasswordResetSetup,
50
- ProdAppHandle,
51
- ProdSeedFn,
52
- RunProdAppAuthOptions,
53
- RunProdAppOptions,
54
- SignupSetup,
55
- } from "./run-prod-app";
56
- export { runProdApp } from "./run-prod-app";
57
70
  export type {
58
71
  ScaffoldAppOptions,
59
72
  ScaffoldAppResult,
@@ -73,4 +86,9 @@ export type {
73
86
  export { scaffoldDeploy } from "./scaffold-deploy";
74
87
  export type { ScaffoldFeatureOptions, ScaffoldFeatureResult } from "./scaffold-feature";
75
88
  export { scaffoldFeature } from "./scaffold-feature";
89
+ export {
90
+ type SetupTestStackFromFeaturesOptions,
91
+ setupTestStackFromFeatures,
92
+ type TestStackPreset,
93
+ } from "./setup-test-stack-from-features";
76
94
  export { renderWelcomeBanner, type WelcomeBannerInput } from "./welcome-banner";