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.
- package/dist/index.mjs +544 -87
- 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 -600
- package/templates/tanstack-start/CLAUDE.md +26 -127
- package/templates/tanstack-start/README.md +20 -7
- package/templates/tanstack-start/docker/init-extensions.sql +11 -0
- package/templates/tanstack-start/docker-compose.yml +1 -0
- package/templates/tanstack-start/package.json +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/admin/modules.ts +3 -1
- package/templates/tanstack-start/src/questpie/server/.generated/factories.ts +10 -9
- package/templates/tanstack-start/src/questpie/server/collections/index.ts +1 -1
- package/templates/tanstack-start/src/questpie/server/config/auth.ts +1 -1
- package/templates/tanstack-start/src/questpie/server/globals/index.ts +1 -1
- package/templates/tanstack-start/src/questpie/server/modules.ts +4 -5
- package/templates/tanstack-start/src/questpie/server/questpie.config.ts +3 -2
- package/templates/tanstack-start/src/routes/__root.tsx +31 -1
- package/templates/tanstack-start/src/routes/api/$.ts +2 -3
- package/templates/tanstack-start/src/routes/index.tsx +97 -0
- package/templates/tanstack-start/vite.config.ts +2 -2
- package/skills/questpie/AGENTS.md +0 -2670
- package/skills/questpie/SKILL.md +0 -260
- package/skills/questpie/references/auth.md +0 -121
- package/skills/questpie/references/business-logic.md +0 -550
- package/skills/questpie/references/codegen-plugin-api.md +0 -382
- package/skills/questpie/references/crud-api.md +0 -378
- package/skills/questpie/references/data-modeling.md +0 -493
- package/skills/questpie/references/extend.md +0 -557
- package/skills/questpie/references/field-types.md +0 -386
- package/skills/questpie/references/infrastructure-adapters.md +0 -545
- package/skills/questpie/references/multi-tenancy.md +0 -364
- package/skills/questpie/references/production.md +0 -475
- package/skills/questpie/references/query-operators.md +0 -125
- package/skills/questpie/references/quickstart.md +0 -564
- package/skills/questpie/references/rules.md +0 -389
- package/skills/questpie/references/tanstack-query.md +0 -520
- package/skills/questpie-admin/AGENTS.md +0 -1508
- package/skills/questpie-admin/SKILL.md +0 -436
- 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
|
@@ -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 {
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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
|
-
|
|
240
|
+
const templateId = args.templateId ?? templates[0].id;
|
|
241
|
+
return withOptionDefaults({
|
|
94
242
|
projectName: args.projectName,
|
|
95
|
-
templateId
|
|
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: "
|
|
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
|
|
251
|
-
return
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}];
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
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(),
|
|
723
|
+
const targetDir = resolve(process.cwd(), resolvedOptions.projectName);
|
|
724
|
+
const continueOnError = resolvedOptions.continueOnError === true;
|
|
292
725
|
if (existsSync(targetDir)) {
|
|
293
|
-
p.log.error(`Directory ${
|
|
726
|
+
p.log.error(`Directory ${resolvedOptions.projectName} already exists.`);
|
|
294
727
|
process.exit(1);
|
|
295
728
|
}
|
|
296
729
|
const vars = {
|
|
297
|
-
projectName:
|
|
298
|
-
databaseName:
|
|
299
|
-
databaseUser:
|
|
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(),
|
|
737
|
+
const templateDir = join(getTemplatesDir(), resolvedOptions.templateId);
|
|
305
738
|
if (!existsSync(templateDir)) {
|
|
306
|
-
spinner.stop(label.error(`Template "${
|
|
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 (
|
|
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
|
|
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 (
|
|
328
|
-
|
|
329
|
-
|
|
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
|
|
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 (
|
|
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 ${
|
|
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
|
-
"#
|
|
370
|
-
runScript("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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));
|