create-questpie 2.0.4 → 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 (152) hide show
  1. package/dist/index.mjs +362 -119
  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 -607
  114. package/templates/tanstack-start/CLAUDE.md +26 -134
  115. package/templates/tanstack-start/README.md +13 -1
  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/src/lib/auth-client.ts +1 -1
  119. package/templates/tanstack-start/src/lib/client.ts +1 -1
  120. package/templates/tanstack-start/src/lib/query.ts +18 -0
  121. package/templates/tanstack-start/src/questpie/server/collections/index.ts +1 -1
  122. package/templates/tanstack-start/src/questpie/server/globals/index.ts +1 -1
  123. package/templates/tanstack-start/src/questpie/server/questpie.config.ts +1 -1
  124. package/templates/tanstack-start/src/routes/__root.tsx +31 -1
  125. package/templates/tanstack-start/src/routes/api/$.ts +1 -1
  126. package/templates/tanstack-start/src/routes/index.tsx +97 -0
  127. package/skills/questpie/AGENTS.md +0 -2871
  128. package/skills/questpie/SKILL.md +0 -293
  129. package/skills/questpie/coverage.json +0 -213
  130. package/skills/questpie/references/auth.md +0 -236
  131. package/skills/questpie/references/business-logic.md +0 -620
  132. package/skills/questpie/references/codegen-plugin-api.md +0 -382
  133. package/skills/questpie/references/crud-api.md +0 -580
  134. package/skills/questpie/references/data-modeling.md +0 -509
  135. package/skills/questpie/references/extend.md +0 -584
  136. package/skills/questpie/references/field-types.md +0 -398
  137. package/skills/questpie/references/infrastructure-adapters.md +0 -720
  138. package/skills/questpie/references/mcp.md +0 -147
  139. package/skills/questpie/references/multi-tenancy.md +0 -363
  140. package/skills/questpie/references/production.md +0 -640
  141. package/skills/questpie/references/query-operators.md +0 -125
  142. package/skills/questpie/references/quickstart.md +0 -562
  143. package/skills/questpie/references/rules.md +0 -454
  144. package/skills/questpie/references/sandbox.md +0 -110
  145. package/skills/questpie/references/tanstack-query.md +0 -543
  146. package/skills/questpie/references/type-inference.md +0 -167
  147. package/skills/questpie/references/workflows.md +0 -155
  148. package/skills/questpie-admin/AGENTS.md +0 -1515
  149. package/skills/questpie-admin/SKILL.md +0 -443
  150. package/skills/questpie-admin/references/blocks.md +0 -331
  151. package/skills/questpie-admin/references/custom-ui.md +0 -305
  152. package/skills/questpie-admin/references/views.md +0 -449
package/dist/index.mjs CHANGED
@@ -4,16 +4,108 @@ import { join, resolve } from "node:path";
4
4
  import { Command } from "commander";
5
5
  import * as p from "@clack/prompts";
6
6
  import pc from "picocolors";
7
- import { execFileSync, execSync } from "node:child_process";
8
- import { cp, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
7
+ import { execFileSync, execSync, spawn } from "node:child_process";
8
+ import { cp, readFile, readdir, rename, writeFile } from "node:fs/promises";
9
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
+ }
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
  }
@@ -114,12 +206,30 @@ function withOptionDefaults(options) {
114
206
  queueAdapter: options.queueAdapter ?? "pg-boss",
115
207
  emailAdapter: options.emailAdapter ?? "console",
116
208
  realtimeAdapter: options.realtimeAdapter ?? "none",
117
- kvAdapter: options.kvAdapter ?? "memory",
118
- includeWorkflows: options.includeWorkflows ?? false
209
+ kvAdapter: options.kvAdapter ?? "memory"
119
210
  };
120
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
+ }
121
231
  async function runPrompts(args) {
122
- const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
232
+ const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY) && !args.fillDefaults;
123
233
  const queueAdapter = assertChoice("queue adapter", args.queueAdapter, queueAdapters);
124
234
  const emailAdapter = assertChoice("email adapter", args.emailAdapter, emailAdapters);
125
235
  const realtimeAdapter = assertChoice("realtime adapter", args.realtimeAdapter, realtimeAdapters);
@@ -127,10 +237,12 @@ async function runPrompts(args) {
127
237
  if (!isInteractive) {
128
238
  if (!args.projectName) throw new Error("Project name is required in non-interactive mode.");
129
239
  if (!isValidPackageName(args.projectName)) throw new Error("Invalid package name (use lowercase, hyphens, no spaces).");
240
+ const templateId = args.templateId ?? templates[0].id;
130
241
  return withOptionDefaults({
131
242
  projectName: args.projectName,
132
- templateId: args.templateId ?? templates[0].id,
243
+ templateId,
133
244
  databaseName: args.databaseName ?? toDbName(args.projectName),
245
+ modules: args.modules ?? resolveModuleIds(templateId, args.requestedModules),
134
246
  installDeps: args.installDeps ?? true,
135
247
  initGit: args.initGit ?? true,
136
248
  installSkills: args.installSkills ?? true,
@@ -139,8 +251,7 @@ async function runPrompts(args) {
139
251
  queueAdapter,
140
252
  emailAdapter,
141
253
  realtimeAdapter,
142
- kvAdapter,
143
- includeWorkflows: args.includeWorkflows ?? false
254
+ kvAdapter
144
255
  });
145
256
  }
146
257
  p.intro(pc.bgCyan(pc.black(" QUESTPIE — Create a new project ")));
@@ -163,7 +274,7 @@ async function runPrompts(args) {
163
274
  if (args.templateId) return Promise.resolve(args.templateId);
164
275
  if (templates.length === 1) return Promise.resolve(templates[0].id);
165
276
  return p.select({
166
- message: "Select a template",
277
+ message: "Runtime",
167
278
  options: templates.map((t) => ({
168
279
  value: t.id,
169
280
  label: t.label,
@@ -171,6 +282,23 @@ async function runPrompts(args) {
171
282
  }))
172
283
  });
173
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
+ },
174
302
  databaseName: ({ results }) => {
175
303
  if (args.databaseName) return Promise.resolve(args.databaseName);
176
304
  const defaultDb = toDbName(results.projectName);
@@ -217,6 +345,7 @@ async function runPrompts(args) {
217
345
  projectName: questions.projectName,
218
346
  templateId: questions.templateId,
219
347
  databaseName: questions.databaseName,
348
+ modules: questions.modules,
220
349
  installDeps: questions.installDeps,
221
350
  initGit: questions.initGit,
222
351
  installSkills: questions.installSkills,
@@ -225,8 +354,7 @@ async function runPrompts(args) {
225
354
  queueAdapter,
226
355
  emailAdapter,
227
356
  realtimeAdapter,
228
- kvAdapter,
229
- includeWorkflows: args.includeWorkflows ?? false
357
+ kvAdapter
230
358
  });
231
359
  }
232
360
 
@@ -234,6 +362,126 @@ async function runPrompts(args) {
234
362
  //#region src/scaffolder.ts
235
363
  const TEMPLATE_VAR_REGEX = /\{\{(\w+)\}\}/g;
236
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
+ /**
237
485
  * Resolves the path to the templates directory.
238
486
  * Works both in dev (src/) and built (dist/) contexts.
239
487
  */
@@ -296,45 +544,6 @@ async function createLocalEnv(targetDir) {
296
544
  const envPath = join(targetDir, ".env");
297
545
  if (existsSync(examplePath) && !existsSync(envPath)) await cp(examplePath, envPath);
298
546
  }
299
- function getSkillSources(targetDir) {
300
- return [{
301
- name: "questpie",
302
- candidates: [
303
- resolve(import.meta.dirname, "..", "skills", "questpie"),
304
- join(targetDir, "node_modules", "questpie", "skills", "questpie"),
305
- resolve(import.meta.dirname, "..", "..", "questpie", "skills", "questpie"),
306
- resolve(import.meta.dirname, "..", "..", "..", "skills", "questpie")
307
- ]
308
- }, {
309
- name: "questpie-admin",
310
- candidates: [
311
- resolve(import.meta.dirname, "..", "skills", "questpie-admin"),
312
- join(targetDir, "node_modules", "@questpie", "admin", "skills", "questpie-admin"),
313
- resolve(import.meta.dirname, "..", "..", "admin", "skills", "questpie-admin"),
314
- resolve(import.meta.dirname, "..", "..", "..", "skills", "questpie-admin")
315
- ]
316
- }];
317
- }
318
- async function installProjectSkills(targetDir) {
319
- const installed = [];
320
- const skillsDir = join(targetDir, ".agents", "skills");
321
- for (const skill of getSkillSources(targetDir)) {
322
- const source = skill.candidates.find((candidate) => existsSync(candidate));
323
- if (!source) continue;
324
- const destination = join(skillsDir, skill.name);
325
- await mkdir(skillsDir, { recursive: true });
326
- await rm(destination, {
327
- recursive: true,
328
- force: true
329
- });
330
- await cp(source, destination, {
331
- recursive: true,
332
- dereference: true
333
- });
334
- installed.push(skill.name);
335
- }
336
- return installed;
337
- }
338
547
  function handleFatalStepFailure(message, error, continueOnError) {
339
548
  if (continueOnError) return;
340
549
  const cause = error instanceof Error ? error.message : typeof error === "string" ? error : String(error);
@@ -345,14 +554,14 @@ async function applyProjectOptions(targetDir, options) {
345
554
  await writeFile(join(targetDir, "src", "lib", "env.ts"), buildEnvFile(options), "utf-8");
346
555
  await writeFile(join(targetDir, "src", "questpie", "server", "questpie.config.ts"), buildRuntimeConfig(options), "utf-8");
347
556
  await writeFile(join(targetDir, "src", "questpie", "server", "modules.ts"), buildServerModules(options), "utf-8");
348
- await writeFile(join(targetDir, "src", "questpie", "admin", "modules.ts"), buildAdminModules(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");
349
559
  }
350
560
  async function updatePackageJson(targetDir, options) {
351
561
  const packageJsonPath = join(targetDir, "package.json");
352
562
  const packageJson = JSON.parse(await readFile(packageJsonPath, "utf-8"));
353
- if (options.queueAdapter === "bullmq") packageJson.dependencies.bullmq = "^5.0.0";
354
- if (options.queueAdapter === "bullmq" || options.realtimeAdapter === "redis-streams" || options.kvAdapter === "redis") packageJson.dependencies.redis = "^5.0.0";
355
- if (options.includeWorkflows) packageJson.dependencies["@questpie/workflows"] = "latest";
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;
356
565
  await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, " ")}\n`);
357
566
  }
358
567
  function buildEnvFile(options) {
@@ -376,27 +585,27 @@ function buildEnvFile(options) {
376
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(),`);
377
586
  if (options.emailAdapter === "resend") lines.push(`\t\tRESEND_API_KEY: z.string().optional(),`);
378
587
  if (options.emailAdapter === "plunk") lines.push(`\t\tPLUNK_SECRET_KEY: z.string().optional(),`);
379
- if (options.queueAdapter === "bullmq" || options.realtimeAdapter === "redis-streams" || options.kvAdapter === "redis") lines.push(`\t\tREDIS_URL: z.string().url().default("redis://localhost:6379"),`);
588
+ for (const line of dedupe(selectedFeatures(options).flatMap((f) => f.envVars ?? []))) lines.push(line);
380
589
  lines.push(`\t},`, `\truntimeEnv: process.env,`, `\temptyStringAsUndefined: true,`, `});`, ``);
381
590
  return lines.join("\n");
382
591
  }
383
592
  function buildRuntimeConfig(options) {
384
- const imports = [`import { runtimeConfig } from "questpie/app";`, `import { ConsoleAdapter } from "questpie/adapters/console";`];
385
- if (options.queueAdapter === "pg-boss") imports.push(`import { pgBossAdapter } from "questpie/adapters/pg-boss";`);
386
- if (options.queueAdapter === "bullmq") imports.push(`import { bullMQAdapter } from "questpie/adapters/bullmq";`);
387
- if (options.emailAdapter === "smtp") imports.push(`import { SmtpAdapter } from "questpie/adapters/smtp";`);
388
- if (options.emailAdapter === "resend") imports.push(`import { ResendAdapter } from "questpie/adapters/resend";`);
389
- if (options.emailAdapter === "plunk") imports.push(`import { PlunkAdapter } from "questpie/adapters/plunk";`);
390
- if (options.realtimeAdapter === "pg-notify") imports.push(`import { pgNotifyAdapter } from "questpie/adapters/pg-notify";`);
391
- if (options.realtimeAdapter === "redis-streams") imports.push(`import { redisStreamsAdapter } from "questpie/adapters/redis-streams";`);
392
- if (options.kvAdapter === "redis") {
393
- imports.push(`import { redisKVAdapter } from "questpie/adapters/redis-kv";`);
394
- imports.push(`import { createClient } from "redis";`);
395
- }
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
+ ];
396
606
  imports.push(``, `import { env } from "@/lib/env.js";`, ``);
397
- const helpers = [];
398
- if (options.emailAdapter === "resend" || options.emailAdapter === "plunk") helpers.push(`function requiredEnv(value: string | undefined, name: string): string {`, `\tif (!value) throw new Error(\`Missing required environment variable: \${name}\`);`, `\treturn value;`, `}`, ``);
399
- if (options.kvAdapter === "redis") helpers.push(`async function getRedis() {`, `\tconst redis = createClient({ url: env.REDIS_URL });`, `\tawait redis.connect();`, `\treturn redis;`, `}`, ``);
607
+ const kvHelper = features.kv?.[options.kvAdapter ?? "memory"]?.configHelper;
608
+ const helpers = [...buildEmailConfigHelper(options), ...kvHelper ?? []];
400
609
  const configEntries = [
401
610
  `\tapp: { url: env.APP_URL },`,
402
611
  `\tdb: { url: env.DATABASE_URL },`,
@@ -405,11 +614,10 @@ function buildRuntimeConfig(options) {
405
614
  `\t\tadapter: ${buildEmailAdapterExpression(options)},`,
406
615
  `\t},`
407
616
  ];
408
- if (options.queueAdapter === "pg-boss") configEntries.push(`\tqueue: {`, `\t\tadapter: pgBossAdapter({ connectionString: env.DATABASE_URL }),`, `\t},`);
409
- if (options.queueAdapter === "bullmq") configEntries.push(`\tqueue: {`, `\t\tadapter: bullMQAdapter({ connection: { url: env.REDIS_URL } }),`, `\t},`);
410
- if (options.realtimeAdapter === "pg-notify") configEntries.push(`\trealtime: {`, `\t\tadapter: pgNotifyAdapter({ connectionString: env.DATABASE_URL }),`, `\t},`);
411
- if (options.realtimeAdapter === "redis-streams") configEntries.push(`\trealtime: {`, `\t\tadapter: redisStreamsAdapter({ url: env.REDIS_URL }),`, `\t},`);
412
- if (options.kvAdapter === "redis") configEntries.push(`\tkv: {`, `\t\tadapter: redisKVAdapter({ client: getRedis, keyPrefix: "${options.projectName}:" }),`, `\t},`);
617
+ for (const { axis, option } of selectedFeatureOptions(options)) {
618
+ const entry = features[axis]?.[option]?.configEntry;
619
+ if (entry) configEntries.push(...entry(options));
620
+ }
413
621
  return [
414
622
  `/**`,
415
623
  ` * QUESTPIE Runtime Configuration`,
@@ -432,24 +640,36 @@ function buildEmailAdapterExpression(options) {
432
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 })`;
433
641
  return `new ConsoleAdapter({ logHtml: false })`;
434
642
  }
435
- function buildServerModules(options) {
436
- const imports = [
437
- `/**`,
438
- ` * Modules static module dependencies for this project.`,
439
- ` */`,
440
- `import { adminModule } from "@questpie/admin/modules/admin";`,
441
- `import { openApiModule } from "@questpie/openapi";`
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
+ ``
442
658
  ];
443
- const modules = ["adminModule", "openApiModule"];
444
- if (options.includeWorkflows) {
445
- imports.push(`import { workflowsModule } from "@questpie/workflows/modules/workflows";`);
446
- modules.push("workflowsModule");
447
- }
659
+ return [];
660
+ }
661
+ function buildServerModules(options) {
662
+ const selected = selectedModules(options);
448
663
  return [
449
- ...imports,
664
+ ...[
665
+ `/**`,
666
+ ` * Modules — static module dependencies for this project.`,
667
+ ` */`,
668
+ ...selected.map((mod) => `import { ${mod.serverSymbol} } from "${mod.serverImport}";`)
669
+ ],
450
670
  ``,
451
671
  `const modules = [`,
452
- ...modules.map((mod) => `\t${mod},`),
672
+ ...selected.map((mod) => `\t${mod.serverSymbol},`),
453
673
  `] as const;`,
454
674
  ``,
455
675
  `export default modules;`,
@@ -457,27 +677,47 @@ function buildServerModules(options) {
457
677
  ].join("\n");
458
678
  }
459
679
  function buildAdminModules(options) {
460
- const imports = [`import { adminClientModule } from "@questpie/admin/client/modules/admin";`];
461
- const modules = ["adminClientModule"];
462
- if (options.includeWorkflows) {
463
- imports.push(`import { workflowsClientModule } from "@questpie/workflows/client/modules/workflows";`);
464
- modules.push("workflowsClientModule");
465
- }
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);
466
683
  return [
467
684
  ...imports,
468
685
  ``,
469
- `export default [${modules.join(", ")}] as const;`,
686
+ `export default [${symbols.join(", ")}] as const;`,
470
687
  ``
471
688
  ].join("\n");
472
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"
706
+ });
707
+ child.on("error", () => {});
708
+ child.unref();
709
+ return true;
710
+ } catch {
711
+ return false;
712
+ }
713
+ }
473
714
  async function scaffold(options) {
474
715
  const resolvedOptions = {
475
716
  ...options,
476
717
  queueAdapter: options.queueAdapter ?? "pg-boss",
477
718
  emailAdapter: options.emailAdapter ?? "console",
478
719
  realtimeAdapter: options.realtimeAdapter ?? "none",
479
- kvAdapter: options.kvAdapter ?? "memory",
480
- includeWorkflows: options.includeWorkflows ?? false
720
+ kvAdapter: options.kvAdapter ?? "memory"
481
721
  };
482
722
  const spinner = p.spinner();
483
723
  const targetDir = resolve(process.cwd(), resolvedOptions.projectName);
@@ -519,16 +759,8 @@ async function scaffold(options) {
519
759
  handleFatalStepFailure("Dependency installation failed", error, continueOnError);
520
760
  }
521
761
  }
522
- if (resolvedOptions.installSkills) {
523
- spinner.start("Installing QUESTPIE agent skills");
524
- try {
525
- const installedSkills = await installProjectSkills(targetDir);
526
- if (installedSkills.length > 0) spinner.stop(label.success(`Installed skills: ${installedSkills.join(", ")}`));
527
- else spinner.stop(label.warn("Could not find packaged skills — run `bunx skill add questpie/questpie` manually if available"));
528
- } catch {
529
- spinner.stop(label.warn("Failed to install skills — continuing"));
530
- }
531
- }
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"));
532
764
  if (resolvedOptions.installDeps && resolvedOptions.runCodegen) {
533
765
  spinner.start("Generating QUESTPIE app");
534
766
  try {
@@ -584,6 +816,10 @@ async function scaffold(options) {
584
816
 
585
817
  //#endregion
586
818
  //#region src/index.ts
819
+ /** Collect a repeatable `--module <name>` flag into an array. */
820
+ function collectModule(value, previous) {
821
+ return [...previous, value];
822
+ }
587
823
  function readPackageVersion() {
588
824
  for (const candidate of [resolve(import.meta.dirname, "..", "package.json"), resolve(import.meta.dirname, "..", "..", "package.json")]) {
589
825
  if (!existsSync(candidate)) continue;
@@ -592,13 +828,21 @@ function readPackageVersion() {
592
828
  }
593
829
  return "0.0.0";
594
830
  }
595
- 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("--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("--workflows", "Install and register @questpie/workflows").option("--continue-on-error", "Keep scaffold files when dependency install or codegen fails").action(async (projectName, opts) => {
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) => {
596
832
  try {
597
- 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
+ }
598
840
  await scaffold(await runPrompts({
599
841
  projectName,
600
- templateId: opts.template,
842
+ templateId,
601
843
  databaseName: opts.database,
844
+ requestedModules: requestedModules.length > 0 ? requestedModules : void 0,
845
+ fillDefaults: opts.yes === true,
602
846
  installDeps: opts.install === false ? false : void 0,
603
847
  initGit: opts.git === false ? false : void 0,
604
848
  installSkills: opts.skills === false ? false : void 0,
@@ -607,8 +851,7 @@ new Command().name("create-questpie").description("Create a new QUESTPIE project
607
851
  queueAdapter: opts.queue,
608
852
  emailAdapter: opts.email,
609
853
  realtimeAdapter: opts.realtime,
610
- kvAdapter: opts.kv,
611
- includeWorkflows: opts.workflows === true
854
+ kvAdapter: opts.kv
612
855
  }));
613
856
  } catch (error) {
614
857
  console.error(error instanceof Error ? error.message : String(error));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-questpie",
3
- "version": "2.0.4",
3
+ "version": "2.1.0",
4
4
  "description": "Create a new QUESTPIE project",
5
5
  "keywords": [
6
6
  "create",
@@ -20,8 +20,7 @@
20
20
  },
21
21
  "files": [
22
22
  "dist",
23
- "templates",
24
- "skills"
23
+ "templates"
25
24
  ],
26
25
  "type": "module",
27
26
  "publishConfig": {