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.
- package/dist/index.mjs +362 -119
- package/package.json +2 -3
- package/templates/elysia/AGENTS.md +56 -0
- package/templates/elysia/CLAUDE.md +39 -0
- package/templates/elysia/Dockerfile +24 -0
- package/templates/elysia/README.md +148 -0
- package/templates/elysia/docker/init-extensions.sql +11 -0
- package/templates/elysia/docker-compose.yml +21 -0
- package/templates/elysia/env.example +16 -0
- package/templates/elysia/gitignore +6 -0
- package/templates/elysia/package.json +47 -0
- package/templates/elysia/questpie.config.ts +12 -0
- package/templates/elysia/src/index.ts +21 -0
- package/templates/elysia/src/lib/auth-client.ts +32 -0
- package/templates/elysia/src/lib/client.ts +13 -0
- package/templates/elysia/src/lib/env.ts +24 -0
- package/templates/elysia/src/lib/query-client.ts +18 -0
- package/templates/elysia/src/lib/query.ts +18 -0
- package/templates/elysia/src/questpie/server/.generated/context.gen.ts +200 -0
- package/templates/elysia/src/questpie/server/.generated/entities.gen.ts +84 -0
- package/templates/elysia/src/questpie/server/.generated/factories.ts +65 -0
- package/templates/elysia/src/questpie/server/.generated/index.ts +131 -0
- package/templates/elysia/src/questpie/server/.generated/names.gen.ts +25 -0
- package/templates/elysia/src/questpie/server/app.ts +10 -0
- package/templates/elysia/src/questpie/server/collections/index.ts +1 -0
- package/templates/elysia/src/questpie/server/collections/posts.collection.ts +10 -0
- package/templates/elysia/src/questpie/server/config/auth.ts +8 -0
- package/templates/elysia/src/questpie/server/config/openapi.ts +10 -0
- package/templates/elysia/src/questpie/server/globals/index.ts +1 -0
- package/templates/elysia/src/questpie/server/globals/site-settings.global.ts +10 -0
- package/templates/elysia/src/questpie/server/modules.ts +8 -0
- package/templates/elysia/src/questpie/server/questpie.config.ts +21 -0
- package/templates/elysia/tsconfig.json +28 -0
- package/templates/hono/AGENTS.md +56 -0
- package/templates/hono/CLAUDE.md +39 -0
- package/templates/hono/Dockerfile +24 -0
- package/templates/hono/README.md +148 -0
- package/templates/hono/docker/init-extensions.sql +11 -0
- package/templates/hono/docker-compose.yml +21 -0
- package/templates/hono/env.example +16 -0
- package/templates/hono/gitignore +6 -0
- package/templates/hono/package.json +47 -0
- package/templates/hono/questpie.config.ts +12 -0
- package/templates/hono/src/index.ts +30 -0
- package/templates/hono/src/lib/auth-client.ts +32 -0
- package/templates/hono/src/lib/client.ts +13 -0
- package/templates/hono/src/lib/env.ts +24 -0
- package/templates/hono/src/lib/query-client.ts +18 -0
- package/templates/hono/src/lib/query.ts +18 -0
- package/templates/hono/src/questpie/server/.generated/context.gen.ts +200 -0
- package/templates/hono/src/questpie/server/.generated/entities.gen.ts +84 -0
- package/templates/hono/src/questpie/server/.generated/factories.ts +65 -0
- package/templates/hono/src/questpie/server/.generated/index.ts +131 -0
- package/templates/hono/src/questpie/server/.generated/names.gen.ts +25 -0
- package/templates/hono/src/questpie/server/app.ts +10 -0
- package/templates/hono/src/questpie/server/collections/index.ts +1 -0
- package/templates/hono/src/questpie/server/collections/posts.collection.ts +10 -0
- package/templates/hono/src/questpie/server/config/auth.ts +8 -0
- package/templates/hono/src/questpie/server/config/openapi.ts +10 -0
- package/templates/hono/src/questpie/server/globals/index.ts +1 -0
- package/templates/hono/src/questpie/server/globals/site-settings.global.ts +10 -0
- package/templates/hono/src/questpie/server/modules.ts +8 -0
- package/templates/hono/src/questpie/server/questpie.config.ts +21 -0
- package/templates/hono/tsconfig.json +28 -0
- package/templates/next/AGENTS.md +55 -0
- package/templates/next/CLAUDE.md +39 -0
- package/templates/next/Dockerfile +25 -0
- package/templates/next/README.md +148 -0
- package/templates/next/components.json +22 -0
- package/templates/next/docker/init-extensions.sql +11 -0
- package/templates/next/docker-compose.yml +21 -0
- package/templates/next/env.example +16 -0
- package/templates/next/gitignore +10 -0
- package/templates/next/next-env.d.ts +5 -0
- package/templates/next/next.config.ts +20 -0
- package/templates/next/package.json +54 -0
- package/templates/next/postcss.config.mjs +8 -0
- package/templates/next/public/.gitkeep +0 -0
- package/templates/next/questpie.config.ts +12 -0
- package/templates/next/src/app/admin/[[...all]]/page.tsx +34 -0
- package/templates/next/src/app/admin/admin.css +4 -0
- package/templates/next/src/app/admin/layout.tsx +63 -0
- package/templates/next/src/app/api/[...all]/route.ts +24 -0
- package/templates/next/src/app/layout.tsx +24 -0
- package/templates/next/src/app/not-found.tsx +18 -0
- package/templates/next/src/app/page.tsx +74 -0
- package/templates/next/src/app/providers.tsx +11 -0
- package/templates/next/src/lib/auth-client.ts +12 -0
- package/templates/next/src/lib/client.ts +13 -0
- package/templates/next/src/lib/env.ts +24 -0
- package/templates/next/src/lib/query-client.ts +18 -0
- package/templates/next/src/lib/query.ts +18 -0
- package/templates/next/src/questpie/admin/.generated/client.ts +13 -0
- package/templates/next/src/questpie/admin/admin.ts +9 -0
- package/templates/next/src/questpie/admin/modules.ts +3 -0
- package/templates/next/src/questpie/server/.generated/context.gen.ts +204 -0
- package/templates/next/src/questpie/server/.generated/entities.gen.ts +100 -0
- package/templates/next/src/questpie/server/.generated/factories.ts +204 -0
- package/templates/next/src/questpie/server/.generated/index.ts +139 -0
- package/templates/next/src/questpie/server/.generated/names.gen.ts +31 -0
- package/templates/next/src/questpie/server/app.ts +10 -0
- package/templates/next/src/questpie/server/collections/index.ts +1 -0
- package/templates/next/src/questpie/server/collections/posts.collection.ts +58 -0
- package/templates/next/src/questpie/server/config/admin.ts +80 -0
- package/templates/next/src/questpie/server/config/auth.ts +8 -0
- package/templates/next/src/questpie/server/config/openapi.ts +10 -0
- package/templates/next/src/questpie/server/globals/index.ts +1 -0
- package/templates/next/src/questpie/server/globals/site-settings.global.ts +19 -0
- package/templates/next/src/questpie/server/modules.ts +9 -0
- package/templates/next/src/questpie/server/questpie.config.ts +21 -0
- package/templates/next/src/styles.css +125 -0
- package/templates/next/tsconfig.json +37 -0
- package/templates/tanstack-start/AGENTS.md +35 -607
- package/templates/tanstack-start/CLAUDE.md +26 -134
- package/templates/tanstack-start/README.md +13 -1
- package/templates/tanstack-start/docker/init-extensions.sql +11 -0
- package/templates/tanstack-start/docker-compose.yml +1 -0
- package/templates/tanstack-start/src/lib/auth-client.ts +1 -1
- package/templates/tanstack-start/src/lib/client.ts +1 -1
- package/templates/tanstack-start/src/lib/query.ts +18 -0
- package/templates/tanstack-start/src/questpie/server/collections/index.ts +1 -1
- package/templates/tanstack-start/src/questpie/server/globals/index.ts +1 -1
- package/templates/tanstack-start/src/questpie/server/questpie.config.ts +1 -1
- package/templates/tanstack-start/src/routes/__root.tsx +31 -1
- package/templates/tanstack-start/src/routes/api/$.ts +1 -1
- package/templates/tanstack-start/src/routes/index.tsx +97 -0
- package/skills/questpie/AGENTS.md +0 -2871
- package/skills/questpie/SKILL.md +0 -293
- package/skills/questpie/coverage.json +0 -213
- package/skills/questpie/references/auth.md +0 -236
- package/skills/questpie/references/business-logic.md +0 -620
- package/skills/questpie/references/codegen-plugin-api.md +0 -382
- package/skills/questpie/references/crud-api.md +0 -580
- package/skills/questpie/references/data-modeling.md +0 -509
- package/skills/questpie/references/extend.md +0 -584
- package/skills/questpie/references/field-types.md +0 -398
- package/skills/questpie/references/infrastructure-adapters.md +0 -720
- package/skills/questpie/references/mcp.md +0 -147
- package/skills/questpie/references/multi-tenancy.md +0 -363
- package/skills/questpie/references/production.md +0 -640
- package/skills/questpie/references/query-operators.md +0 -125
- package/skills/questpie/references/quickstart.md +0 -562
- package/skills/questpie/references/rules.md +0 -454
- package/skills/questpie/references/sandbox.md +0 -110
- package/skills/questpie/references/tanstack-query.md +0 -543
- package/skills/questpie/references/type-inference.md +0 -167
- package/skills/questpie/references/workflows.md +0 -155
- package/skills/questpie-admin/AGENTS.md +0 -1515
- package/skills/questpie-admin/SKILL.md +0 -443
- package/skills/questpie-admin/references/blocks.md +0 -331
- package/skills/questpie-admin/references/custom-ui.md +0 -305
- 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,
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
354
|
-
|
|
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
|
-
|
|
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
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
|
398
|
-
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
659
|
+
return [];
|
|
660
|
+
}
|
|
661
|
+
function buildServerModules(options) {
|
|
662
|
+
const selected = selectedModules(options);
|
|
448
663
|
return [
|
|
449
|
-
...
|
|
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
|
-
...
|
|
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
|
|
461
|
-
const
|
|
462
|
-
|
|
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 [${
|
|
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
|
-
|
|
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("--
|
|
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
|
-
|
|
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
|
|
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
|
|
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": {
|