create-questpie 2.0.3 → 2.1.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 (153) hide show
  1. package/dist/index.mjs +544 -87
  2. package/package.json +2 -3
  3. package/templates/elysia/AGENTS.md +56 -0
  4. package/templates/elysia/CLAUDE.md +39 -0
  5. package/templates/elysia/Dockerfile +24 -0
  6. package/templates/elysia/README.md +148 -0
  7. package/templates/elysia/docker/init-extensions.sql +11 -0
  8. package/templates/elysia/docker-compose.yml +21 -0
  9. package/templates/elysia/env.example +16 -0
  10. package/templates/elysia/gitignore +6 -0
  11. package/templates/elysia/package.json +47 -0
  12. package/templates/elysia/questpie.config.ts +12 -0
  13. package/templates/elysia/src/index.ts +21 -0
  14. package/templates/elysia/src/lib/auth-client.ts +32 -0
  15. package/templates/elysia/src/lib/client.ts +13 -0
  16. package/templates/elysia/src/lib/env.ts +24 -0
  17. package/templates/elysia/src/lib/query-client.ts +18 -0
  18. package/templates/elysia/src/lib/query.ts +18 -0
  19. package/templates/elysia/src/questpie/server/.generated/context.gen.ts +200 -0
  20. package/templates/elysia/src/questpie/server/.generated/entities.gen.ts +84 -0
  21. package/templates/elysia/src/questpie/server/.generated/factories.ts +65 -0
  22. package/templates/elysia/src/questpie/server/.generated/index.ts +131 -0
  23. package/templates/elysia/src/questpie/server/.generated/names.gen.ts +25 -0
  24. package/templates/elysia/src/questpie/server/app.ts +10 -0
  25. package/templates/elysia/src/questpie/server/collections/index.ts +1 -0
  26. package/templates/elysia/src/questpie/server/collections/posts.collection.ts +10 -0
  27. package/templates/elysia/src/questpie/server/config/auth.ts +8 -0
  28. package/templates/elysia/src/questpie/server/config/openapi.ts +10 -0
  29. package/templates/elysia/src/questpie/server/globals/index.ts +1 -0
  30. package/templates/elysia/src/questpie/server/globals/site-settings.global.ts +10 -0
  31. package/templates/elysia/src/questpie/server/modules.ts +8 -0
  32. package/templates/elysia/src/questpie/server/questpie.config.ts +21 -0
  33. package/templates/elysia/tsconfig.json +28 -0
  34. package/templates/hono/AGENTS.md +56 -0
  35. package/templates/hono/CLAUDE.md +39 -0
  36. package/templates/hono/Dockerfile +24 -0
  37. package/templates/hono/README.md +148 -0
  38. package/templates/hono/docker/init-extensions.sql +11 -0
  39. package/templates/hono/docker-compose.yml +21 -0
  40. package/templates/hono/env.example +16 -0
  41. package/templates/hono/gitignore +6 -0
  42. package/templates/hono/package.json +47 -0
  43. package/templates/hono/questpie.config.ts +12 -0
  44. package/templates/hono/src/index.ts +30 -0
  45. package/templates/hono/src/lib/auth-client.ts +32 -0
  46. package/templates/hono/src/lib/client.ts +13 -0
  47. package/templates/hono/src/lib/env.ts +24 -0
  48. package/templates/hono/src/lib/query-client.ts +18 -0
  49. package/templates/hono/src/lib/query.ts +18 -0
  50. package/templates/hono/src/questpie/server/.generated/context.gen.ts +200 -0
  51. package/templates/hono/src/questpie/server/.generated/entities.gen.ts +84 -0
  52. package/templates/hono/src/questpie/server/.generated/factories.ts +65 -0
  53. package/templates/hono/src/questpie/server/.generated/index.ts +131 -0
  54. package/templates/hono/src/questpie/server/.generated/names.gen.ts +25 -0
  55. package/templates/hono/src/questpie/server/app.ts +10 -0
  56. package/templates/hono/src/questpie/server/collections/index.ts +1 -0
  57. package/templates/hono/src/questpie/server/collections/posts.collection.ts +10 -0
  58. package/templates/hono/src/questpie/server/config/auth.ts +8 -0
  59. package/templates/hono/src/questpie/server/config/openapi.ts +10 -0
  60. package/templates/hono/src/questpie/server/globals/index.ts +1 -0
  61. package/templates/hono/src/questpie/server/globals/site-settings.global.ts +10 -0
  62. package/templates/hono/src/questpie/server/modules.ts +8 -0
  63. package/templates/hono/src/questpie/server/questpie.config.ts +21 -0
  64. package/templates/hono/tsconfig.json +28 -0
  65. package/templates/next/AGENTS.md +55 -0
  66. package/templates/next/CLAUDE.md +39 -0
  67. package/templates/next/Dockerfile +25 -0
  68. package/templates/next/README.md +148 -0
  69. package/templates/next/components.json +22 -0
  70. package/templates/next/docker/init-extensions.sql +11 -0
  71. package/templates/next/docker-compose.yml +21 -0
  72. package/templates/next/env.example +16 -0
  73. package/templates/next/gitignore +10 -0
  74. package/templates/next/next-env.d.ts +5 -0
  75. package/templates/next/next.config.ts +20 -0
  76. package/templates/next/package.json +54 -0
  77. package/templates/next/postcss.config.mjs +8 -0
  78. package/templates/next/public/.gitkeep +0 -0
  79. package/templates/next/questpie.config.ts +12 -0
  80. package/templates/next/src/app/admin/[[...all]]/page.tsx +34 -0
  81. package/templates/next/src/app/admin/admin.css +4 -0
  82. package/templates/next/src/app/admin/layout.tsx +63 -0
  83. package/templates/next/src/app/api/[...all]/route.ts +24 -0
  84. package/templates/next/src/app/layout.tsx +24 -0
  85. package/templates/next/src/app/not-found.tsx +18 -0
  86. package/templates/next/src/app/page.tsx +74 -0
  87. package/templates/next/src/app/providers.tsx +11 -0
  88. package/templates/next/src/lib/auth-client.ts +12 -0
  89. package/templates/next/src/lib/client.ts +13 -0
  90. package/templates/next/src/lib/env.ts +24 -0
  91. package/templates/next/src/lib/query-client.ts +18 -0
  92. package/templates/next/src/lib/query.ts +18 -0
  93. package/templates/next/src/questpie/admin/.generated/client.ts +13 -0
  94. package/templates/next/src/questpie/admin/admin.ts +9 -0
  95. package/templates/next/src/questpie/admin/modules.ts +3 -0
  96. package/templates/next/src/questpie/server/.generated/context.gen.ts +204 -0
  97. package/templates/next/src/questpie/server/.generated/entities.gen.ts +100 -0
  98. package/templates/next/src/questpie/server/.generated/factories.ts +204 -0
  99. package/templates/next/src/questpie/server/.generated/index.ts +139 -0
  100. package/templates/next/src/questpie/server/.generated/names.gen.ts +31 -0
  101. package/templates/next/src/questpie/server/app.ts +10 -0
  102. package/templates/next/src/questpie/server/collections/index.ts +1 -0
  103. package/templates/next/src/questpie/server/collections/posts.collection.ts +58 -0
  104. package/templates/next/src/questpie/server/config/admin.ts +80 -0
  105. package/templates/next/src/questpie/server/config/auth.ts +8 -0
  106. package/templates/next/src/questpie/server/config/openapi.ts +10 -0
  107. package/templates/next/src/questpie/server/globals/index.ts +1 -0
  108. package/templates/next/src/questpie/server/globals/site-settings.global.ts +19 -0
  109. package/templates/next/src/questpie/server/modules.ts +9 -0
  110. package/templates/next/src/questpie/server/questpie.config.ts +21 -0
  111. package/templates/next/src/styles.css +125 -0
  112. package/templates/next/tsconfig.json +37 -0
  113. package/templates/tanstack-start/AGENTS.md +35 -600
  114. package/templates/tanstack-start/CLAUDE.md +26 -127
  115. package/templates/tanstack-start/README.md +20 -7
  116. package/templates/tanstack-start/docker/init-extensions.sql +11 -0
  117. package/templates/tanstack-start/docker-compose.yml +1 -0
  118. package/templates/tanstack-start/package.json +1 -0
  119. package/templates/tanstack-start/src/lib/auth-client.ts +1 -1
  120. package/templates/tanstack-start/src/lib/client.ts +1 -1
  121. package/templates/tanstack-start/src/lib/query.ts +18 -0
  122. package/templates/tanstack-start/src/questpie/admin/modules.ts +3 -1
  123. package/templates/tanstack-start/src/questpie/server/.generated/factories.ts +10 -9
  124. package/templates/tanstack-start/src/questpie/server/collections/index.ts +1 -1
  125. package/templates/tanstack-start/src/questpie/server/config/auth.ts +1 -1
  126. package/templates/tanstack-start/src/questpie/server/globals/index.ts +1 -1
  127. package/templates/tanstack-start/src/questpie/server/modules.ts +4 -5
  128. package/templates/tanstack-start/src/questpie/server/questpie.config.ts +3 -2
  129. package/templates/tanstack-start/src/routes/__root.tsx +31 -1
  130. package/templates/tanstack-start/src/routes/api/$.ts +2 -3
  131. package/templates/tanstack-start/src/routes/index.tsx +97 -0
  132. package/templates/tanstack-start/vite.config.ts +2 -2
  133. package/skills/questpie/AGENTS.md +0 -2670
  134. package/skills/questpie/SKILL.md +0 -260
  135. package/skills/questpie/references/auth.md +0 -121
  136. package/skills/questpie/references/business-logic.md +0 -550
  137. package/skills/questpie/references/codegen-plugin-api.md +0 -382
  138. package/skills/questpie/references/crud-api.md +0 -378
  139. package/skills/questpie/references/data-modeling.md +0 -493
  140. package/skills/questpie/references/extend.md +0 -557
  141. package/skills/questpie/references/field-types.md +0 -386
  142. package/skills/questpie/references/infrastructure-adapters.md +0 -545
  143. package/skills/questpie/references/multi-tenancy.md +0 -364
  144. package/skills/questpie/references/production.md +0 -475
  145. package/skills/questpie/references/query-operators.md +0 -125
  146. package/skills/questpie/references/quickstart.md +0 -564
  147. package/skills/questpie/references/rules.md +0 -389
  148. package/skills/questpie/references/tanstack-query.md +0 -520
  149. package/skills/questpie-admin/AGENTS.md +0 -1508
  150. package/skills/questpie-admin/SKILL.md +0 -436
  151. package/skills/questpie-admin/references/blocks.md +0 -331
  152. package/skills/questpie-admin/references/custom-ui.md +0 -305
  153. package/skills/questpie-admin/references/views.md +0 -449
package/dist/index.mjs CHANGED
@@ -1,19 +1,111 @@
1
1
  #!/usr/bin/env node
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
2
4
  import { Command } from "commander";
3
5
  import * as p from "@clack/prompts";
4
6
  import pc from "picocolors";
5
- import { execFileSync, execSync } from "node:child_process";
6
- import { existsSync } from "node:fs";
7
- import { cp, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
8
- import { join, resolve } from "node:path";
7
+ import { execFileSync, execSync, spawn } from "node:child_process";
8
+ import { cp, readFile, readdir, rename, writeFile } from "node:fs/promises";
9
+
10
+ //#region src/modules.ts
11
+ /** Runtimes that ship a render layer (admin UI can mount). */
12
+ const RENDER_RUNTIMES = ["tanstack-start", "next"];
13
+ /**
14
+ * The three already-wired modules. Import paths + symbols are copied verbatim
15
+ * from the proven scaffolder emission (`buildServerModules` / `buildAdminModules`)
16
+ * and the tanstack-start template's `server/modules.ts` + `admin/modules.ts`.
17
+ */
18
+ const modules = [
19
+ {
20
+ id: "admin",
21
+ label: "Admin",
22
+ hint: "admin UI panel",
23
+ group: "Core",
24
+ defaultFor: ["tanstack-start", "next"],
25
+ requiresRender: true,
26
+ deps: { "@questpie/admin": "latest" },
27
+ serverImport: "@questpie/admin/modules/admin",
28
+ serverSymbol: "adminModule",
29
+ clientImport: "@questpie/admin/client/modules/admin",
30
+ clientSymbol: "adminClientModule"
31
+ },
32
+ {
33
+ id: "openapi",
34
+ label: "OpenAPI",
35
+ hint: "REST + Scalar docs at /api/docs",
36
+ group: "Core",
37
+ defaultFor: [
38
+ "tanstack-start",
39
+ "next",
40
+ "hono",
41
+ "elysia"
42
+ ],
43
+ deps: { "@questpie/openapi": "latest" },
44
+ serverImport: "@questpie/openapi",
45
+ serverSymbol: "openApiModule"
46
+ },
47
+ {
48
+ id: "workflows",
49
+ label: "Workflows",
50
+ hint: "durable workflow engine",
51
+ group: "Optional",
52
+ deps: { "@questpie/workflows": "latest" },
53
+ serverImport: "@questpie/workflows/modules/workflows",
54
+ serverSymbol: "workflowsModule",
55
+ clientImport: "@questpie/workflows/client/modules/workflows",
56
+ clientSymbol: "workflowsClientModule"
57
+ }
58
+ ];
59
+ function getModule(id) {
60
+ return modules.find((m) => m.id === id);
61
+ }
62
+ /** Default module ids for a runtime posture (drives prompt pre-selection). */
63
+ function defaultModuleIds(runtime) {
64
+ return modules.filter((m) => m.defaultFor?.includes(runtime)).map((m) => m.id);
65
+ }
66
+ /**
67
+ * Compatibility oracle — single source of truth for "can this module run on
68
+ * this runtime?". Reused by the prompt filter AND flag validation so there is
69
+ * zero rule duplication. Pure, no side effects.
70
+ *
71
+ * Rule (v1): a `requiresRender` module (admin) needs a render-layer runtime
72
+ * (tanstack-start | next). Headless runtimes (hono | elysia) reject it.
73
+ */
74
+ function isModuleAllowed(moduleId, runtimeId) {
75
+ const mod = getModule(moduleId);
76
+ if (!mod) return false;
77
+ if (mod.requiresRender) return RENDER_RUNTIMES.includes(runtimeId);
78
+ return true;
79
+ }
9
80
 
81
+ //#endregion
10
82
  //#region src/templates.ts
11
- const templates = [{
12
- id: "tanstack-start",
13
- label: "TanStack Start",
14
- hint: "recommended",
15
- description: "Full-stack React with TanStack Start, Vite, Tailwind CSS, and Nitro server"
16
- }];
83
+ const templates = [
84
+ {
85
+ id: "tanstack-start",
86
+ label: "TanStack Start",
87
+ hint: "recommended",
88
+ description: "Full-stack React with TanStack Start, Vite, Tailwind CSS, and Nitro server"
89
+ },
90
+ {
91
+ id: "hono",
92
+ label: "Hono",
93
+ hint: "headless api",
94
+ description: "Headless REST API with Hono on Bun, OpenAPI/Scalar docs, and a typed client (no admin UI)"
95
+ },
96
+ {
97
+ id: "elysia",
98
+ label: "Elysia",
99
+ hint: "headless api",
100
+ description: "Headless REST API with Elysia on Bun, OpenAPI/Scalar docs, and a typed client (no admin UI)"
101
+ },
102
+ {
103
+ id: "next",
104
+ label: "Next.js",
105
+ hint: "fullstack",
106
+ description: "Full-stack React with Next.js App Router (Turbopack), admin UI, OpenAPI/Scalar docs, and a typed client"
107
+ }
108
+ ];
17
109
  function getTemplate(id) {
18
110
  return templates.find((t) => t.id === id);
19
111
  }
@@ -86,19 +178,81 @@ const label = {
86
178
 
87
179
  //#endregion
88
180
  //#region src/prompts.ts
181
+ const queueAdapters = [
182
+ "pg-boss",
183
+ "bullmq",
184
+ "none"
185
+ ];
186
+ const emailAdapters = [
187
+ "console",
188
+ "smtp",
189
+ "resend",
190
+ "plunk"
191
+ ];
192
+ const realtimeAdapters = [
193
+ "none",
194
+ "pg-notify",
195
+ "redis-streams"
196
+ ];
197
+ const kvAdapters = ["memory", "redis"];
198
+ function assertChoice(name, value, choices) {
199
+ if (value === void 0) return void 0;
200
+ if (choices.includes(value)) return value;
201
+ throw new Error(`Invalid ${name}: ${value}. Expected one of: ${choices.join(", ")}.`);
202
+ }
203
+ function withOptionDefaults(options) {
204
+ return {
205
+ ...options,
206
+ queueAdapter: options.queueAdapter ?? "pg-boss",
207
+ emailAdapter: options.emailAdapter ?? "console",
208
+ realtimeAdapter: options.realtimeAdapter ?? "none",
209
+ kvAdapter: options.kvAdapter ?? "memory"
210
+ };
211
+ }
212
+ /**
213
+ * Resolve the module ids for a non-interactive run (flags / --yes). Explicit
214
+ * `--module(s)` win; otherwise fall back to the runtime defaults. Every id is
215
+ * validated through the same `isModuleAllowed` oracle the prompt filter uses,
216
+ * so a flag combo cannot bypass the compatibility rule.
217
+ */
218
+ function resolveModuleIds(runtime, requested) {
219
+ const allowed = modules.filter((m) => isModuleAllowed(m.id, runtime));
220
+ const allowedIds = new Set(allowed.map((m) => m.id));
221
+ if (requested && requested.length > 0) {
222
+ for (const id of requested) if (!allowedIds.has(id)) {
223
+ const known = modules.some((m) => m.id === id);
224
+ throw new Error(known ? `Module "${id}" is not available for runtime "${runtime}".` : `Unknown module: ${id}. Expected one of: ${modules.map((m) => m.id).join(", ")}.`);
225
+ }
226
+ const wanted = new Set(requested);
227
+ return allowed.filter((m) => wanted.has(m.id)).map((m) => m.id);
228
+ }
229
+ return defaultModuleIds(runtime).filter((id) => allowedIds.has(id));
230
+ }
89
231
  async function runPrompts(args) {
90
- if (!Boolean(process.stdin.isTTY && process.stdout.isTTY)) {
232
+ const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !args.fillDefaults;
233
+ const queueAdapter = assertChoice("queue adapter", args.queueAdapter, queueAdapters);
234
+ const emailAdapter = assertChoice("email adapter", args.emailAdapter, emailAdapters);
235
+ const realtimeAdapter = assertChoice("realtime adapter", args.realtimeAdapter, realtimeAdapters);
236
+ const kvAdapter = assertChoice("KV adapter", args.kvAdapter, kvAdapters);
237
+ if (!isInteractive) {
91
238
  if (!args.projectName) throw new Error("Project name is required in non-interactive mode.");
92
239
  if (!isValidPackageName(args.projectName)) throw new Error("Invalid package name (use lowercase, hyphens, no spaces).");
93
- return {
240
+ const templateId = args.templateId ?? templates[0].id;
241
+ return withOptionDefaults({
94
242
  projectName: args.projectName,
95
- templateId: args.templateId ?? templates[0].id,
243
+ templateId,
96
244
  databaseName: args.databaseName ?? toDbName(args.projectName),
245
+ modules: args.modules ?? resolveModuleIds(templateId, args.requestedModules),
97
246
  installDeps: args.installDeps ?? true,
98
247
  initGit: args.initGit ?? true,
99
248
  installSkills: args.installSkills ?? true,
100
- runCodegen: args.runCodegen ?? true
101
- };
249
+ runCodegen: args.runCodegen ?? true,
250
+ continueOnError: args.continueOnError ?? false,
251
+ queueAdapter,
252
+ emailAdapter,
253
+ realtimeAdapter,
254
+ kvAdapter
255
+ });
102
256
  }
103
257
  p.intro(pc.bgCyan(pc.black(" QUESTPIE — Create a new project ")));
104
258
  const questions = await p.group({
@@ -120,7 +274,7 @@ async function runPrompts(args) {
120
274
  if (args.templateId) return Promise.resolve(args.templateId);
121
275
  if (templates.length === 1) return Promise.resolve(templates[0].id);
122
276
  return p.select({
123
- message: "Select a template",
277
+ message: "Runtime",
124
278
  options: templates.map((t) => ({
125
279
  value: t.id,
126
280
  label: t.label,
@@ -128,6 +282,23 @@ async function runPrompts(args) {
128
282
  }))
129
283
  });
130
284
  },
285
+ modules: ({ results }) => {
286
+ const runtime = results.templateId;
287
+ if (args.modules) return Promise.resolve(args.modules);
288
+ if (args.requestedModules) return Promise.resolve(resolveModuleIds(runtime, args.requestedModules));
289
+ const available = modules.filter((m) => isModuleAllowed(m.id, runtime));
290
+ const defaults = new Set(defaultModuleIds(runtime));
291
+ return p.multiselect({
292
+ message: "Modules",
293
+ required: false,
294
+ initialValues: available.filter((m) => defaults.has(m.id)).map((m) => m.id),
295
+ options: available.map((m) => ({
296
+ value: m.id,
297
+ label: m.group ? `${m.group} · ${m.label}` : m.label,
298
+ hint: m.hint
299
+ }))
300
+ });
301
+ },
131
302
  databaseName: ({ results }) => {
132
303
  if (args.databaseName) return Promise.resolve(args.databaseName);
133
304
  const defaultDb = toDbName(results.projectName);
@@ -170,21 +341,147 @@ async function runPrompts(args) {
170
341
  p.cancel("Operation cancelled.");
171
342
  process.exit(0);
172
343
  } });
173
- return {
344
+ return withOptionDefaults({
174
345
  projectName: questions.projectName,
175
346
  templateId: questions.templateId,
176
347
  databaseName: questions.databaseName,
348
+ modules: questions.modules,
177
349
  installDeps: questions.installDeps,
178
350
  initGit: questions.initGit,
179
351
  installSkills: questions.installSkills,
180
- runCodegen: questions.runCodegen
181
- };
352
+ runCodegen: questions.runCodegen,
353
+ continueOnError: args.continueOnError ?? false,
354
+ queueAdapter,
355
+ emailAdapter,
356
+ realtimeAdapter,
357
+ kvAdapter
358
+ });
182
359
  }
183
360
 
184
361
  //#endregion
185
362
  //#region src/scaffolder.ts
186
363
  const TEMPLATE_VAR_REGEX = /\{\{(\w+)\}\}/g;
187
364
  /**
365
+ * Central pinned dependency versions. One place to bump every package the
366
+ * scaffolder can add. Module deps reference these keys too — `depVersion`
367
+ * resolves a package name to its pinned version (falling back to the version
368
+ * declared on the module entry, then "latest").
369
+ */
370
+ const dependencyVersionMap = {
371
+ "@questpie/admin": "latest",
372
+ "@questpie/openapi": "latest",
373
+ "@questpie/workflows": "latest",
374
+ bullmq: "^5.0.0",
375
+ redis: "^5.0.0"
376
+ };
377
+ function depVersion(name, fallback = "latest") {
378
+ return dependencyVersionMap[name] ?? fallback;
379
+ }
380
+ /** Stable de-duplication preserving first-seen order. */
381
+ function dedupe(lines) {
382
+ return Array.from(new Set(lines));
383
+ }
384
+ const REDIS_URL_ENV = `\t\tREDIS_URL: z.string().url().default("redis://localhost:6379"),`;
385
+ const features = {
386
+ queue: {
387
+ "pg-boss": {
388
+ configImports: [`import { pgBossAdapter } from "questpie/adapters/pg-boss";`],
389
+ configEntry: () => [
390
+ `\tqueue: {`,
391
+ `\t\tadapter: pgBossAdapter({ connectionString: env.DATABASE_URL }),`,
392
+ `\t},`
393
+ ]
394
+ },
395
+ bullmq: {
396
+ deps: {
397
+ bullmq: depVersion("bullmq"),
398
+ redis: depVersion("redis")
399
+ },
400
+ envVars: [REDIS_URL_ENV],
401
+ configImports: [`import { bullMQAdapter } from "questpie/adapters/bullmq";`],
402
+ configEntry: () => [
403
+ `\tqueue: {`,
404
+ `\t\tadapter: bullMQAdapter({ connection: { url: env.REDIS_URL } }),`,
405
+ `\t},`
406
+ ]
407
+ },
408
+ none: {}
409
+ },
410
+ realtime: {
411
+ none: {},
412
+ "pg-notify": {
413
+ configImports: [`import { pgNotifyAdapter } from "questpie/adapters/pg-notify";`],
414
+ configEntry: () => [
415
+ `\trealtime: {`,
416
+ `\t\tadapter: pgNotifyAdapter({ connectionString: env.DATABASE_URL }),`,
417
+ `\t},`
418
+ ]
419
+ },
420
+ "redis-streams": {
421
+ deps: { redis: depVersion("redis") },
422
+ envVars: [REDIS_URL_ENV],
423
+ configImports: [`import { redisStreamsAdapter } from "questpie/adapters/redis-streams";`],
424
+ configEntry: () => [
425
+ `\trealtime: {`,
426
+ `\t\tadapter: redisStreamsAdapter({ url: env.REDIS_URL }),`,
427
+ `\t},`
428
+ ]
429
+ }
430
+ },
431
+ kv: {
432
+ memory: {},
433
+ redis: {
434
+ deps: { redis: depVersion("redis") },
435
+ envVars: [REDIS_URL_ENV],
436
+ configImports: [`import { redisKVAdapter } from "questpie/adapters/redis-kv";`, `import { createClient } from "redis";`],
437
+ configHelper: [
438
+ `async function getRedis() {`,
439
+ `\tconst redis = createClient({ url: env.REDIS_URL });`,
440
+ `\tawait redis.connect();`,
441
+ `\treturn redis;`,
442
+ `}`,
443
+ ``
444
+ ],
445
+ configEntry: (options) => [
446
+ `\tkv: {`,
447
+ `\t\tadapter: redisKVAdapter({ client: getRedis, keyPrefix: "${options.projectName}:" }),`,
448
+ `\t},`
449
+ ]
450
+ }
451
+ }
452
+ };
453
+ /** The adapter selection for each feature axis, with the same defaults the generators assume. */
454
+ function selectedFeatureOptions(options) {
455
+ return [
456
+ {
457
+ axis: "queue",
458
+ option: options.queueAdapter ?? "pg-boss"
459
+ },
460
+ {
461
+ axis: "realtime",
462
+ option: options.realtimeAdapter ?? "none"
463
+ },
464
+ {
465
+ axis: "kv",
466
+ option: options.kvAdapter ?? "memory"
467
+ }
468
+ ];
469
+ }
470
+ /** Resolve the selected feature descriptors (skips unknown/missing entries). */
471
+ function selectedFeatures(options) {
472
+ return selectedFeatureOptions(options).map(({ axis, option }) => features[axis]?.[option]).filter((f) => Boolean(f));
473
+ }
474
+ /**
475
+ * The modules selected for this project. The selection is resolved upstream
476
+ * (prompts / flags, validated through the `isModuleAllowed` oracle) and threaded
477
+ * through `options.modules`; here we just project it onto registry entries,
478
+ * preserving registry order.
479
+ */
480
+ function selectedModules(options) {
481
+ const ids = new Set(options.modules);
482
+ return modules.filter((m) => ids.has(m.id));
483
+ }
484
+ /**
188
485
  * Resolves the path to the templates directory.
189
486
  * Works both in dev (src/) and built (dist/) contexts.
190
487
  */
@@ -247,63 +544,199 @@ async function createLocalEnv(targetDir) {
247
544
  const envPath = join(targetDir, ".env");
248
545
  if (existsSync(examplePath) && !existsSync(envPath)) await cp(examplePath, envPath);
249
546
  }
250
- function getSkillSources(targetDir) {
251
- return [{
252
- name: "questpie",
253
- candidates: [
254
- resolve(import.meta.dirname, "..", "skills", "questpie"),
255
- join(targetDir, "node_modules", "questpie", "skills", "questpie"),
256
- resolve(import.meta.dirname, "..", "..", "questpie", "skills", "questpie"),
257
- resolve(import.meta.dirname, "..", "..", "..", "skills", "questpie")
258
- ]
259
- }, {
260
- name: "questpie-admin",
261
- candidates: [
262
- resolve(import.meta.dirname, "..", "skills", "questpie-admin"),
263
- join(targetDir, "node_modules", "@questpie", "admin", "skills", "questpie-admin"),
264
- resolve(import.meta.dirname, "..", "..", "admin", "skills", "questpie-admin"),
265
- resolve(import.meta.dirname, "..", "..", "..", "skills", "questpie-admin")
266
- ]
267
- }];
268
- }
269
- async function installProjectSkills(targetDir) {
270
- const installed = [];
271
- const skillsDir = join(targetDir, ".agents", "skills");
272
- for (const skill of getSkillSources(targetDir)) {
273
- const source = skill.candidates.find((candidate) => existsSync(candidate));
274
- if (!source) continue;
275
- const destination = join(skillsDir, skill.name);
276
- await mkdir(skillsDir, { recursive: true });
277
- await rm(destination, {
278
- recursive: true,
279
- force: true
280
- });
281
- await cp(source, destination, {
282
- recursive: true,
283
- dereference: true
547
+ function handleFatalStepFailure(message, error, continueOnError) {
548
+ if (continueOnError) return;
549
+ const cause = error instanceof Error ? error.message : typeof error === "string" ? error : String(error);
550
+ throw new Error(`${message}: ${cause}`);
551
+ }
552
+ async function applyProjectOptions(targetDir, options) {
553
+ await updatePackageJson(targetDir, options);
554
+ await writeFile(join(targetDir, "src", "lib", "env.ts"), buildEnvFile(options), "utf-8");
555
+ await writeFile(join(targetDir, "src", "questpie", "server", "questpie.config.ts"), buildRuntimeConfig(options), "utf-8");
556
+ await writeFile(join(targetDir, "src", "questpie", "server", "modules.ts"), buildServerModules(options), "utf-8");
557
+ const adminDir = join(targetDir, "src", "questpie", "admin");
558
+ if (options.modules.includes("admin") && existsSync(adminDir)) await writeFile(join(adminDir, "modules.ts"), buildAdminModules(options), "utf-8");
559
+ }
560
+ async function updatePackageJson(targetDir, options) {
561
+ const packageJsonPath = join(targetDir, "package.json");
562
+ const packageJson = JSON.parse(await readFile(packageJsonPath, "utf-8"));
563
+ for (const mod of selectedModules(options)) for (const [name, version] of Object.entries(mod.deps)) packageJson.dependencies[name] = depVersion(name, version);
564
+ for (const feature of selectedFeatures(options)) for (const [name, version] of Object.entries(feature.deps ?? {})) packageJson.dependencies[name] = version;
565
+ await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, " ")}\n`);
566
+ }
567
+ function buildEnvFile(options) {
568
+ const mailAdapters = Array.from(new Set(["console", options.emailAdapter ?? "console"]));
569
+ const lines = [
570
+ `import { createEnv } from "@t3-oss/env-core";`,
571
+ `import { z } from "zod";`,
572
+ ``,
573
+ `export const env = createEnv({`,
574
+ `\tserver: {`,
575
+ `\t\tDATABASE_URL: z.string().url(),`,
576
+ `\t\tAPP_URL: z.string().url().default("http://localhost:3000"),`,
577
+ `\t\tPORT: z`,
578
+ `\t\t\t.string()`,
579
+ `\t\t\t.transform(Number)`,
580
+ `\t\t\t.pipe(z.number().int().positive())`,
581
+ `\t\t\t.default(3000),`,
582
+ `\t\tBETTER_AUTH_SECRET: z.string().min(1).default("change-me-in-production"),`,
583
+ `\t\tMAIL_ADAPTER: z.enum(${JSON.stringify(mailAdapters)}).default("console"),`
584
+ ];
585
+ if (options.emailAdapter === "smtp") lines.push(`\t\tSMTP_HOST: z.string().optional(),`, `\t\tSMTP_PORT: z`, `\t\t\t.string()`, `\t\t\t.transform(Number)`, `\t\t\t.pipe(z.number().int().positive())`, `\t\t\t.optional(),`);
586
+ if (options.emailAdapter === "resend") lines.push(`\t\tRESEND_API_KEY: z.string().optional(),`);
587
+ if (options.emailAdapter === "plunk") lines.push(`\t\tPLUNK_SECRET_KEY: z.string().optional(),`);
588
+ for (const line of dedupe(selectedFeatures(options).flatMap((f) => f.envVars ?? []))) lines.push(line);
589
+ lines.push(`\t},`, `\truntimeEnv: process.env,`, `\temptyStringAsUndefined: true,`, `});`, ``);
590
+ return lines.join("\n");
591
+ }
592
+ function buildRuntimeConfig(options) {
593
+ const queueImports = features.queue?.[options.queueAdapter ?? "pg-boss"]?.configImports ?? [];
594
+ const realtimeImports = features.realtime?.[options.realtimeAdapter ?? "none"]?.configImports ?? [];
595
+ const kvImports = features.kv?.[options.kvAdapter ?? "memory"]?.configImports ?? [];
596
+ const imports = [
597
+ `import { runtimeConfig } from "questpie/app";`,
598
+ `import { ConsoleAdapter } from "questpie/adapters/console";`,
599
+ ...dedupe([
600
+ ...queueImports,
601
+ ...buildEmailConfigImports(options),
602
+ ...realtimeImports,
603
+ ...kvImports
604
+ ])
605
+ ];
606
+ imports.push(``, `import { env } from "@/lib/env.js";`, ``);
607
+ const kvHelper = features.kv?.[options.kvAdapter ?? "memory"]?.configHelper;
608
+ const helpers = [...buildEmailConfigHelper(options), ...kvHelper ?? []];
609
+ const configEntries = [
610
+ `\tapp: { url: env.APP_URL },`,
611
+ `\tdb: { url: env.DATABASE_URL },`,
612
+ `\tstorage: { basePath: "/api" },`,
613
+ `\temail: {`,
614
+ `\t\tadapter: ${buildEmailAdapterExpression(options)},`,
615
+ `\t},`
616
+ ];
617
+ for (const { axis, option } of selectedFeatureOptions(options)) {
618
+ const entry = features[axis]?.[option]?.configEntry;
619
+ if (entry) configEntries.push(...entry(options));
620
+ }
621
+ return [
622
+ `/**`,
623
+ ` * QUESTPIE Runtime Configuration`,
624
+ ` *`,
625
+ ` * Runtime-only configuration: database, adapters, secrets.`,
626
+ ` * Entity definitions are codegen-generated.`,
627
+ ` */`,
628
+ ``,
629
+ ...imports,
630
+ ...helpers,
631
+ `export default runtimeConfig({`,
632
+ ...configEntries,
633
+ `});`,
634
+ ``
635
+ ].join("\n");
636
+ }
637
+ function buildEmailAdapterExpression(options) {
638
+ if (options.emailAdapter === "smtp") return `env.MAIL_ADAPTER === "smtp"\n\t\t\t? new SmtpAdapter({\n\t\t\t\t\ttransport: {\n\t\t\t\t\t\thost: env.SMTP_HOST || "localhost",\n\t\t\t\t\t\tport: env.SMTP_PORT || 1025,\n\t\t\t\t\t\tsecure: false,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t: new ConsoleAdapter({ logHtml: false })`;
639
+ if (options.emailAdapter === "resend") return `env.MAIL_ADAPTER === "resend"\n\t\t\t? new ResendAdapter({ apiKey: requiredEnv(env.RESEND_API_KEY, "RESEND_API_KEY") })\n\t\t\t: new ConsoleAdapter({ logHtml: false })`;
640
+ if (options.emailAdapter === "plunk") return `env.MAIL_ADAPTER === "plunk"\n\t\t\t? new PlunkAdapter({ apiKey: requiredEnv(env.PLUNK_SECRET_KEY, "PLUNK_SECRET_KEY") })\n\t\t\t: new ConsoleAdapter({ logHtml: false })`;
641
+ return `new ConsoleAdapter({ logHtml: false })`;
642
+ }
643
+ /** Email adapter import lines for `questpie.config.ts` (ConsoleAdapter is always imported separately). */
644
+ function buildEmailConfigImports(options) {
645
+ if (options.emailAdapter === "smtp") return [`import { SmtpAdapter } from "questpie/adapters/smtp";`];
646
+ if (options.emailAdapter === "resend") return [`import { ResendAdapter } from "questpie/adapters/resend";`];
647
+ if (options.emailAdapter === "plunk") return [`import { PlunkAdapter } from "questpie/adapters/plunk";`];
648
+ return [];
649
+ }
650
+ /** Email helper block (`requiredEnv`) for adapters that read required secrets. */
651
+ function buildEmailConfigHelper(options) {
652
+ if (options.emailAdapter === "resend" || options.emailAdapter === "plunk") return [
653
+ `function requiredEnv(value: string | undefined, name: string): string {`,
654
+ `\tif (!value) throw new Error(\`Missing required environment variable: \${name}\`);`,
655
+ `\treturn value;`,
656
+ `}`,
657
+ ``
658
+ ];
659
+ return [];
660
+ }
661
+ function buildServerModules(options) {
662
+ const selected = selectedModules(options);
663
+ return [
664
+ ...[
665
+ `/**`,
666
+ ` * Modules — static module dependencies for this project.`,
667
+ ` */`,
668
+ ...selected.map((mod) => `import { ${mod.serverSymbol} } from "${mod.serverImport}";`)
669
+ ],
670
+ ``,
671
+ `const modules = [`,
672
+ ...selected.map((mod) => `\t${mod.serverSymbol},`),
673
+ `] as const;`,
674
+ ``,
675
+ `export default modules;`,
676
+ ``
677
+ ].join("\n");
678
+ }
679
+ function buildAdminModules(options) {
680
+ const clientModules = selectedModules(options).filter((mod) => mod.clientImport && mod.clientSymbol);
681
+ const imports = clientModules.map((mod) => `import { ${mod.clientSymbol} } from "${mod.clientImport}";`);
682
+ const symbols = clientModules.map((mod) => mod.clientSymbol);
683
+ return [
684
+ ...imports,
685
+ ``,
686
+ `export default [${symbols.join(", ")}] as const;`,
687
+ ``
688
+ ].join("\n");
689
+ }
690
+ /**
691
+ * Spawn `bunx skills add questpie/questpie` detached in the target project so
692
+ * the scaffold can complete without blocking on the (network-bound) install.
693
+ * Returns false when the process can't even be spawned (no `bunx` on PATH etc.)
694
+ * so the caller can fall back to printing the manual command.
695
+ */
696
+ function startSkillsInstall(targetDir) {
697
+ try {
698
+ const child = spawn("bunx", [
699
+ "skills",
700
+ "add",
701
+ "questpie/questpie"
702
+ ], {
703
+ cwd: targetDir,
704
+ detached: true,
705
+ stdio: "ignore"
284
706
  });
285
- installed.push(skill.name);
707
+ child.on("error", () => {});
708
+ child.unref();
709
+ return true;
710
+ } catch {
711
+ return false;
286
712
  }
287
- return installed;
288
713
  }
289
714
  async function scaffold(options) {
715
+ const resolvedOptions = {
716
+ ...options,
717
+ queueAdapter: options.queueAdapter ?? "pg-boss",
718
+ emailAdapter: options.emailAdapter ?? "console",
719
+ realtimeAdapter: options.realtimeAdapter ?? "none",
720
+ kvAdapter: options.kvAdapter ?? "memory"
721
+ };
290
722
  const spinner = p.spinner();
291
- const targetDir = resolve(process.cwd(), options.projectName);
723
+ const targetDir = resolve(process.cwd(), resolvedOptions.projectName);
724
+ const continueOnError = resolvedOptions.continueOnError === true;
292
725
  if (existsSync(targetDir)) {
293
- p.log.error(`Directory ${options.projectName} already exists.`);
726
+ p.log.error(`Directory ${resolvedOptions.projectName} already exists.`);
294
727
  process.exit(1);
295
728
  }
296
729
  const vars = {
297
- projectName: options.projectName,
298
- databaseName: options.databaseName,
299
- databaseUser: options.databaseName,
730
+ projectName: resolvedOptions.projectName,
731
+ databaseName: resolvedOptions.databaseName,
732
+ databaseUser: resolvedOptions.databaseName,
300
733
  databasePassword: generatePassword(),
301
734
  authSecret: generatePassword(48)
302
735
  };
303
736
  spinner.start("Copying template files");
304
- const templateDir = join(getTemplatesDir(), options.templateId);
737
+ const templateDir = join(getTemplatesDir(), resolvedOptions.templateId);
305
738
  if (!existsSync(templateDir)) {
306
- spinner.stop(label.error(`Template "${options.templateId}" not found`));
739
+ spinner.stop(label.error(`Template "${resolvedOptions.templateId}" not found`));
307
740
  process.exit(1);
308
741
  }
309
742
  await cp(templateDir, targetDir, { recursive: true });
@@ -313,37 +746,32 @@ async function scaffold(options) {
313
746
  await renameEnvExample(targetDir);
314
747
  await processDirectory(targetDir, vars);
315
748
  await createLocalEnv(targetDir);
749
+ await applyProjectOptions(targetDir, resolvedOptions);
316
750
  spinner.stop(label.success("Processed template variables"));
317
751
  const pm = detectPackageManager();
318
- if (options.installDeps) {
752
+ if (resolvedOptions.installDeps) {
319
753
  spinner.start(`Installing dependencies with ${pm}`);
320
754
  try {
321
755
  installDependencies(targetDir, pm);
322
756
  spinner.stop(label.success("Installed dependencies"));
323
- } catch {
324
- spinner.stop(label.warn("Failed to install dependencies — run manually"));
757
+ } catch (error) {
758
+ spinner.stop(label.warn("Failed to install dependencies"));
759
+ handleFatalStepFailure("Dependency installation failed", error, continueOnError);
325
760
  }
326
761
  }
327
- if (options.installSkills) {
328
- spinner.start("Installing QUESTPIE agent skills");
329
- try {
330
- const installedSkills = await installProjectSkills(targetDir);
331
- if (installedSkills.length > 0) spinner.stop(label.success(`Installed skills: ${installedSkills.join(", ")}`));
332
- else spinner.stop(label.warn("Could not find packaged skills — run `bunx skill add questpie/questpie` manually if available"));
333
- } catch {
334
- spinner.stop(label.warn("Failed to install skills — continuing"));
335
- }
336
- }
337
- if (options.installDeps && options.runCodegen) {
762
+ if (resolvedOptions.installSkills) if (startSkillsInstall(targetDir)) p.log.info(label.info("Installing QUESTPIE agent skills in the background (`bunx skills add questpie/questpie`)"));
763
+ else p.log.warn(label.warn("Could not start skills install — run `bunx skills add questpie/questpie` in the project"));
764
+ if (resolvedOptions.installDeps && resolvedOptions.runCodegen) {
338
765
  spinner.start("Generating QUESTPIE app");
339
766
  try {
340
767
  runPackageScript(targetDir, pm, "scaffold:generate");
341
768
  spinner.stop(label.success("Generated QUESTPIE app"));
342
- } catch {
343
- spinner.stop(label.warn("Failed to run codegen — run manually"));
769
+ } catch (error) {
770
+ spinner.stop(label.warn("Failed to run codegen"));
771
+ handleFatalStepFailure("QUESTPIE codegen failed", error, continueOnError);
344
772
  }
345
773
  }
346
- if (options.initGit && isGitInstalled()) {
774
+ if (resolvedOptions.initGit && isGitInstalled()) {
347
775
  spinner.start("Initializing git repository");
348
776
  try {
349
777
  gitInit(targetDir);
@@ -355,7 +783,7 @@ async function scaffold(options) {
355
783
  const runScript = (script) => pm === "npm" ? `npm run ${script}` : `${pm} run ${script}`;
356
784
  const questpieBin = pm === "npm" ? "npx questpie" : "bunx questpie";
357
785
  p.note([
358
- `cd ${options.projectName}`,
786
+ `cd ${resolvedOptions.projectName}`,
359
787
  "",
360
788
  "# Review the generated environment",
361
789
  "# .env has already been created from .env.example",
@@ -366,8 +794,8 @@ async function scaffold(options) {
366
794
  "# Regenerate and type-check the scaffold",
367
795
  runScript("scaffold:verify"),
368
796
  "",
369
- "# Run migrations",
370
- runScript("migrate"),
797
+ "# Create local database tables",
798
+ runScript("db:push"),
371
799
  "",
372
800
  "# Start dev server",
373
801
  runScript("dev"),
@@ -377,24 +805,53 @@ async function scaffold(options) {
377
805
  `${questpieBin} add global marketing`,
378
806
  "",
379
807
  "# If you create files manually",
380
- runScript("questpie:generate")
808
+ runScript("questpie:generate"),
809
+ "",
810
+ "# For production migrations",
811
+ runScript("migrate:create"),
812
+ runScript("migrate")
381
813
  ].join("\n"), "Next steps");
382
814
  p.outro(`${label.success("Done!")} Happy building with QUESTPIE!`);
383
815
  }
384
816
 
385
817
  //#endregion
386
818
  //#region src/index.ts
387
- new Command().name("create-questpie").description("Create a new QUESTPIE project").version("2.0.1").argument("[project-name]", "Name of the project").option("-t, --template <name>", "Template to use (default: tanstack-start)").option("--database <name>", "Database name (default: derived from project name)").option("--no-install", "Skip dependency installation").option("--no-git", "Skip git initialization").option("--no-skills", "Skip installing project-local QUESTPIE agent skills").option("--no-generate", "Skip running QUESTPIE codegen after install").action(async (projectName, opts) => {
819
+ /** Collect a repeatable `--module <name>` flag into an array. */
820
+ function collectModule(value, previous) {
821
+ return [...previous, value];
822
+ }
823
+ function readPackageVersion() {
824
+ for (const candidate of [resolve(import.meta.dirname, "..", "package.json"), resolve(import.meta.dirname, "..", "..", "package.json")]) {
825
+ if (!existsSync(candidate)) continue;
826
+ const packageJson = JSON.parse(readFileSync(candidate, "utf-8"));
827
+ if (packageJson.version) return packageJson.version;
828
+ }
829
+ return "0.0.0";
830
+ }
831
+ new Command().name("create-questpie").description("Create a new QUESTPIE project").version(readPackageVersion()).argument("[project-name]", "Name of the project").option("-t, --template <name>", "Template to use (default: tanstack-start)").option("--runtime <id>", "Runtime to use (alias of --template)").option("--module <name>", "Module to enable (repeatable, e.g. --module admin --module openapi)", collectModule, []).option("--modules <a,b,c>", "Comma-separated modules to enable (e.g. --modules admin,openapi)").option("-y, --yes", "Skip prompts and accept all defaults").option("--database <name>", "Database name (default: derived from project name)").option("--no-install", "Skip dependency installation").option("--no-git", "Skip git initialization").option("--no-skills", "Skip installing project-local QUESTPIE agent skills").option("--no-generate", "Skip running QUESTPIE codegen after install").option("--queue <adapter>", "Queue adapter: pg-boss, bullmq, none (default: pg-boss)").option("--email <adapter>", "Email adapters to scaffold: console, smtp, resend, plunk (default: console)").option("--realtime <adapter>", "Realtime adapter: none, pg-notify, redis-streams (default: none)").option("--kv <adapter>", "KV adapter: memory, redis (default: memory)").option("--continue-on-error", "Keep scaffold files when dependency install or codegen fails").action(async (projectName, opts) => {
388
832
  try {
389
- if (opts.template && !getTemplate(opts.template)) throw new Error(`Unknown template: ${opts.template}`);
833
+ const templateId = opts.template ?? opts.runtime;
834
+ if (templateId && !getTemplate(templateId)) throw new Error(`Unknown ${opts.runtime ? "runtime" : "template"}: ${templateId}`);
835
+ const requestedModules = [...opts.module ?? [], ...typeof opts.modules === "string" ? opts.modules.split(",").map((m) => m.trim()).filter(Boolean) : []];
836
+ if (requestedModules.length > 0 && templateId) for (const id of requestedModules) {
837
+ if (!modules.some((m) => m.id === id)) throw new Error(`Unknown module: ${id}. Available: ${modules.map((m) => m.id).join(", ")}.`);
838
+ if (!isModuleAllowed(id, templateId)) throw new Error(`Module "${id}" is not available for runtime "${templateId}".`);
839
+ }
390
840
  await scaffold(await runPrompts({
391
841
  projectName,
392
- templateId: opts.template,
842
+ templateId,
393
843
  databaseName: opts.database,
844
+ requestedModules: requestedModules.length > 0 ? requestedModules : void 0,
845
+ fillDefaults: opts.yes === true,
394
846
  installDeps: opts.install === false ? false : void 0,
395
847
  initGit: opts.git === false ? false : void 0,
396
848
  installSkills: opts.skills === false ? false : void 0,
397
- runCodegen: opts.generate === false ? false : void 0
849
+ runCodegen: opts.generate === false ? false : void 0,
850
+ continueOnError: opts.continueOnError === true,
851
+ queueAdapter: opts.queue,
852
+ emailAdapter: opts.email,
853
+ realtimeAdapter: opts.realtime,
854
+ kvAdapter: opts.kv
398
855
  }));
399
856
  } catch (error) {
400
857
  console.error(error instanceof Error ? error.message : String(error));