create-questpie 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +81 -0
  2. package/dist/index.mjs +284 -0
  3. package/package.json +44 -0
  4. package/templates/tanstack-start/AGENTS.md +563 -0
  5. package/templates/tanstack-start/CLAUDE.md +105 -0
  6. package/templates/tanstack-start/Dockerfile +23 -0
  7. package/templates/tanstack-start/README.md +94 -0
  8. package/templates/tanstack-start/components.json +22 -0
  9. package/templates/tanstack-start/docker-compose.yml +20 -0
  10. package/templates/tanstack-start/env.example +16 -0
  11. package/templates/tanstack-start/gitignore +12 -0
  12. package/templates/tanstack-start/package.json +43 -0
  13. package/templates/tanstack-start/questpie.config.ts +12 -0
  14. package/templates/tanstack-start/src/admin.css +4 -0
  15. package/templates/tanstack-start/src/lib/auth-client.ts +12 -0
  16. package/templates/tanstack-start/src/lib/cms-client.ts +12 -0
  17. package/templates/tanstack-start/src/lib/env.ts +27 -0
  18. package/templates/tanstack-start/src/lib/query-client.ts +9 -0
  19. package/templates/tanstack-start/src/migrations/index.ts +8 -0
  20. package/templates/tanstack-start/src/questpie/admin/admin.ts +5 -0
  21. package/templates/tanstack-start/src/questpie/admin/builder.ts +4 -0
  22. package/templates/tanstack-start/src/questpie/server/app.ts +52 -0
  23. package/templates/tanstack-start/src/questpie/server/builder.ts +4 -0
  24. package/templates/tanstack-start/src/questpie/server/collections/index.ts +1 -0
  25. package/templates/tanstack-start/src/questpie/server/collections/posts.collection.ts +72 -0
  26. package/templates/tanstack-start/src/questpie/server/dashboard.ts +68 -0
  27. package/templates/tanstack-start/src/questpie/server/globals/index.ts +1 -0
  28. package/templates/tanstack-start/src/questpie/server/globals/site-settings.global.ts +24 -0
  29. package/templates/tanstack-start/src/questpie/server/rpc.ts +4 -0
  30. package/templates/tanstack-start/src/questpie/server/sidebar.ts +26 -0
  31. package/templates/tanstack-start/src/router.tsx +10 -0
  32. package/templates/tanstack-start/src/routes/__root.tsx +16 -0
  33. package/templates/tanstack-start/src/routes/admin/$.tsx +21 -0
  34. package/templates/tanstack-start/src/routes/admin/index.tsx +18 -0
  35. package/templates/tanstack-start/src/routes/admin/login.tsx +17 -0
  36. package/templates/tanstack-start/src/routes/admin.tsx +68 -0
  37. package/templates/tanstack-start/src/routes/api/cms/$.ts +45 -0
  38. package/templates/tanstack-start/src/styles.css +125 -0
  39. package/templates/tanstack-start/tsconfig.json +27 -0
  40. package/templates/tanstack-start/vite.config.ts +26 -0
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # create-questpie
2
+
3
+ Interactive CLI for scaffolding new QUESTPIE projects.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ bunx create-questpie
9
+ ```
10
+
11
+ Or with a project name:
12
+
13
+ ```bash
14
+ bunx create-questpie my-app
15
+ ```
16
+
17
+ ## Options
18
+
19
+ | Flag | Description |
20
+ | ----------------------- | ------------------------------------------- |
21
+ | `-t, --template <name>` | Template to use (default: `tanstack-start`) |
22
+ | `--no-install` | Skip dependency installation |
23
+ | `--no-git` | Skip git initialization |
24
+
25
+ ## Templates
26
+
27
+ ### tanstack-start (default)
28
+
29
+ Full-stack TypeScript project with:
30
+
31
+ - **TanStack Start** — File-based routing with SSR
32
+ - **QUESTPIE** — Collections, globals, auth, storage, jobs pre-configured
33
+ - **@questpie/admin** — Admin panel with sidebar, dashboard, and form views
34
+ - **Tailwind CSS v4** — Styling with shadcn components
35
+ - **Drizzle ORM** — Migrations and typed database access
36
+ - **Vite** — Dev server with HMR
37
+
38
+ The template includes example collections, a site settings global, admin config, and everything wired together — ready to run with `bun dev`.
39
+
40
+ ## What It Creates
41
+
42
+ ```
43
+ my-app/
44
+ ├── src/
45
+ │ ├── questpie/
46
+ │ │ ├── server/
47
+ │ │ │ ├── builder.ts # q.use(adminModule) setup
48
+ │ │ │ ├── cms.ts # CMS assembly + build
49
+ │ │ │ ├── collections/ # Collection definitions
50
+ │ │ │ └── globals/ # Global definitions
51
+ │ │ └── admin/
52
+ │ │ └── admin.ts # Client admin builder
53
+ │ ├── lib/
54
+ │ │ ├── cms-client.ts # Typed CMS client
55
+ │ │ └── query-client.ts # TanStack Query client
56
+ │ ├── routes/
57
+ │ │ ├── api/cms.ts # CMS route handler
58
+ │ │ └── admin/ # Admin panel routes
59
+ │ └── migrations/ # Drizzle migrations
60
+ ├── questpie.config.ts # CLI config
61
+ ├── AGENTS.md # AI agent guidance
62
+ ├── package.json
63
+ └── vite.config.ts
64
+ ```
65
+
66
+ ## After Scaffolding
67
+
68
+ ```bash
69
+ cd my-app
70
+ cp .env.example .env # Set DATABASE_URL
71
+ bun questpie migrate # Run migrations
72
+ bun dev # Start dev server
73
+ ```
74
+
75
+ ## Documentation
76
+
77
+ Full documentation: [https://questpie.com/docs/getting-started/quickstart](https://questpie.com/docs/getting-started/quickstart)
78
+
79
+ ## License
80
+
81
+ MIT
package/dist/index.mjs ADDED
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import * as p from "@clack/prompts";
4
+ import pc from "picocolors";
5
+ import { execSync } from "node:child_process";
6
+ import { existsSync } from "node:fs";
7
+ import { cp, readFile, readdir, rename, writeFile } from "node:fs/promises";
8
+ import { join, resolve } from "node:path";
9
+
10
+ //#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
+ }];
17
+ function getTemplate(id) {
18
+ return templates.find((t) => t.id === id);
19
+ }
20
+
21
+ //#endregion
22
+ //#region src/utils.ts
23
+ function toDbName(str) {
24
+ return str.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_|_$/g, "");
25
+ }
26
+ function isValidPackageName(name) {
27
+ return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(name);
28
+ }
29
+ function generatePassword(length = 24) {
30
+ const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
31
+ let result = "";
32
+ const randomBytes = new Uint8Array(length);
33
+ crypto.getRandomValues(randomBytes);
34
+ for (let i = 0; i < length; i++) result += chars[randomBytes[i] % 62];
35
+ return result;
36
+ }
37
+ function isGitInstalled() {
38
+ try {
39
+ execSync("git --version", { stdio: "ignore" });
40
+ return true;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+ function gitInit(cwd) {
46
+ execSync("git init", {
47
+ cwd,
48
+ stdio: "ignore"
49
+ });
50
+ execSync("git add -A", {
51
+ cwd,
52
+ stdio: "ignore"
53
+ });
54
+ execSync("git commit -m \"Initial commit from create-questpie\"", {
55
+ cwd,
56
+ stdio: "ignore"
57
+ });
58
+ }
59
+ function installDependencies(cwd, packageManager) {
60
+ execSync(packageManager === "npm" ? "npm install" : `${packageManager} install`, {
61
+ cwd,
62
+ stdio: "inherit"
63
+ });
64
+ }
65
+ function detectPackageManager() {
66
+ const userAgent = process.env.npm_config_user_agent;
67
+ if (userAgent) {
68
+ if (userAgent.startsWith("bun")) return "bun";
69
+ if (userAgent.startsWith("pnpm")) return "pnpm";
70
+ if (userAgent.startsWith("yarn")) return "yarn";
71
+ }
72
+ return "bun";
73
+ }
74
+ const label = {
75
+ info: (msg) => `${pc.cyan("ℹ")} ${msg}`,
76
+ success: (msg) => `${pc.green("✓")} ${msg}`,
77
+ warn: (msg) => `${pc.yellow("⚠")} ${msg}`,
78
+ error: (msg) => `${pc.red("✗")} ${msg}`
79
+ };
80
+
81
+ //#endregion
82
+ //#region src/prompts.ts
83
+ async function runPrompts(args) {
84
+ p.intro(pc.bgCyan(pc.black(" QUESTPIE — Create a new project ")));
85
+ const questions = await p.group({
86
+ projectName: () => {
87
+ if (args.projectName) return Promise.resolve(args.projectName);
88
+ return p.text({
89
+ message: "Project name",
90
+ placeholder: "my-questpie-app",
91
+ validate: (value) => {
92
+ if (!value) return "Project name is required";
93
+ if (!isValidPackageName(value)) return "Invalid package name (use lowercase, hyphens, no spaces)";
94
+ }
95
+ });
96
+ },
97
+ templateId: () => {
98
+ if (args.templateId) return Promise.resolve(args.templateId);
99
+ if (templates.length === 1) return Promise.resolve(templates[0].id);
100
+ return p.select({
101
+ message: "Select a template",
102
+ options: templates.map((t) => ({
103
+ value: t.id,
104
+ label: t.label,
105
+ hint: t.hint
106
+ }))
107
+ });
108
+ },
109
+ databaseName: ({ results }) => {
110
+ if (args.databaseName) return Promise.resolve(args.databaseName);
111
+ const defaultDb = toDbName(results.projectName);
112
+ return p.text({
113
+ message: "Database name",
114
+ placeholder: defaultDb,
115
+ defaultValue: defaultDb
116
+ });
117
+ },
118
+ installDeps: () => {
119
+ if (args.installDeps !== void 0) return Promise.resolve(args.installDeps);
120
+ return p.confirm({
121
+ message: "Install dependencies?",
122
+ initialValue: true
123
+ });
124
+ },
125
+ initGit: () => {
126
+ if (args.initGit !== void 0) return Promise.resolve(args.initGit);
127
+ return p.confirm({
128
+ message: "Initialize git repository?",
129
+ initialValue: true
130
+ });
131
+ }
132
+ }, { onCancel: () => {
133
+ p.cancel("Operation cancelled.");
134
+ process.exit(0);
135
+ } });
136
+ return {
137
+ projectName: questions.projectName,
138
+ templateId: questions.templateId,
139
+ databaseName: questions.databaseName,
140
+ installDeps: questions.installDeps,
141
+ initGit: questions.initGit
142
+ };
143
+ }
144
+
145
+ //#endregion
146
+ //#region src/scaffolder.ts
147
+ const TEMPLATE_VAR_REGEX = /\{\{(\w+)\}\}/g;
148
+ /**
149
+ * Resolves the path to the templates directory.
150
+ * Works both in dev (src/) and built (dist/) contexts.
151
+ */
152
+ function getTemplatesDir() {
153
+ const fromDist = resolve(import.meta.dirname, "..", "templates");
154
+ if (existsSync(fromDist)) return fromDist;
155
+ const fromSrc = resolve(import.meta.dirname, "..", "..", "templates");
156
+ if (existsSync(fromSrc)) return fromSrc;
157
+ throw new Error("Could not find templates directory");
158
+ }
159
+ const TEXT_EXTENSIONS = new Set([
160
+ ".ts",
161
+ ".tsx",
162
+ ".js",
163
+ ".jsx",
164
+ ".json",
165
+ ".md",
166
+ ".css",
167
+ ".html",
168
+ ".yml",
169
+ ".yaml",
170
+ ".toml",
171
+ ".env",
172
+ ".example",
173
+ ".hbs",
174
+ ""
175
+ ]);
176
+ function isTextFile(filename) {
177
+ const ext = filename.slice(filename.lastIndexOf("."));
178
+ if (filename.startsWith(".")) return true;
179
+ return TEXT_EXTENSIONS.has(ext);
180
+ }
181
+ async function replaceInFile(filePath, vars) {
182
+ const content = await readFile(filePath, "utf-8");
183
+ const replaced = content.replace(TEMPLATE_VAR_REGEX, (match, key) => {
184
+ return key in vars ? vars[key] : match;
185
+ });
186
+ if (replaced !== content) await writeFile(filePath, replaced, "utf-8");
187
+ }
188
+ async function processDirectory(dir, vars) {
189
+ const entries = await readdir(dir, { withFileTypes: true });
190
+ for (const entry of entries) {
191
+ const fullPath = join(dir, entry.name);
192
+ if (entry.isDirectory()) {
193
+ if (entry.name === "node_modules" || entry.name === ".git") continue;
194
+ await processDirectory(fullPath, vars);
195
+ } else if (entry.isFile() && isTextFile(entry.name)) await replaceInFile(fullPath, vars);
196
+ }
197
+ }
198
+ async function renameGitignore(targetDir) {
199
+ const gitignorePath = join(targetDir, "gitignore");
200
+ if (existsSync(gitignorePath)) await rename(gitignorePath, join(targetDir, ".gitignore"));
201
+ }
202
+ async function renameEnvExample(targetDir) {
203
+ const envPath = join(targetDir, "env.example");
204
+ if (existsSync(envPath)) await rename(envPath, join(targetDir, ".env.example"));
205
+ }
206
+ async function scaffold(options) {
207
+ const spinner = p.spinner();
208
+ const targetDir = resolve(process.cwd(), options.projectName);
209
+ if (existsSync(targetDir)) {
210
+ p.log.error(`Directory ${options.projectName} already exists.`);
211
+ process.exit(1);
212
+ }
213
+ const vars = {
214
+ projectName: options.projectName,
215
+ databaseName: options.databaseName,
216
+ databaseUser: options.databaseName,
217
+ databasePassword: generatePassword()
218
+ };
219
+ spinner.start("Copying template files");
220
+ const templateDir = join(getTemplatesDir(), options.templateId);
221
+ if (!existsSync(templateDir)) {
222
+ spinner.stop(label.error(`Template "${options.templateId}" not found`));
223
+ process.exit(1);
224
+ }
225
+ await cp(templateDir, targetDir, { recursive: true });
226
+ spinner.stop(label.success("Copied template files"));
227
+ spinner.start("Processing template");
228
+ await renameGitignore(targetDir);
229
+ await renameEnvExample(targetDir);
230
+ await processDirectory(targetDir, vars);
231
+ spinner.stop(label.success("Processed template variables"));
232
+ if (options.installDeps) {
233
+ const pm$1 = detectPackageManager();
234
+ spinner.start(`Installing dependencies with ${pm$1}`);
235
+ try {
236
+ installDependencies(targetDir, pm$1);
237
+ spinner.stop(label.success("Installed dependencies"));
238
+ } catch {
239
+ spinner.stop(label.warn("Failed to install dependencies — run manually"));
240
+ }
241
+ }
242
+ if (options.initGit && isGitInstalled()) {
243
+ spinner.start("Initializing git repository");
244
+ try {
245
+ gitInit(targetDir);
246
+ spinner.stop(label.success("Initialized git repository"));
247
+ } catch {
248
+ spinner.stop(label.warn("Failed to initialize git — run manually"));
249
+ }
250
+ }
251
+ const pm = detectPackageManager();
252
+ const runCmd = pm === "npm" ? "npm run" : pm;
253
+ p.note([
254
+ `cd ${options.projectName}`,
255
+ "",
256
+ "# Start PostgreSQL",
257
+ "docker compose up -d",
258
+ "",
259
+ "# Run migrations",
260
+ `${runCmd} questpie migrate`,
261
+ "",
262
+ "# Start dev server",
263
+ `${runCmd} dev`
264
+ ].join("\n"), "Next steps");
265
+ p.outro(`${label.success("Done!")} Happy building with QUESTPIE!`);
266
+ }
267
+
268
+ //#endregion
269
+ //#region src/index.ts
270
+ new Command().name("create-questpie").description("Create a new QUESTPIE CMS project").version("0.1.0").argument("[project-name]", "Name of the project").option("-t, --template <name>", "Template to use (default: tanstack-start)").option("--no-install", "Skip dependency installation").option("--no-git", "Skip git initialization").action(async (projectName, opts) => {
271
+ if (opts.template && !getTemplate(opts.template)) {
272
+ console.error(`Unknown template: ${opts.template}`);
273
+ process.exit(1);
274
+ }
275
+ await scaffold(await runPrompts({
276
+ projectName,
277
+ templateId: opts.template,
278
+ installDeps: opts.install === false ? false : void 0,
279
+ initGit: opts.git === false ? false : void 0
280
+ }));
281
+ }).parse();
282
+
283
+ //#endregion
284
+ export { };
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "create-questpie",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "Create a new QUESTPIE CMS project",
6
+ "bin": {
7
+ "create-questpie": "./dist/index.mjs"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "templates"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsdown",
15
+ "dev": "tsdown --watch",
16
+ "check-types": "tsc --noEmit"
17
+ },
18
+ "dependencies": {
19
+ "@clack/prompts": "^0.10.0",
20
+ "commander": "^13.0.0",
21
+ "picocolors": "^1.1.1"
22
+ },
23
+ "devDependencies": {
24
+ "bun-types": "latest",
25
+ "tsdown": "^0.18.3",
26
+ "typescript": "^5.9.2"
27
+ },
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "https://github.com/questpie/questpie-cms.git",
31
+ "directory": "packages/create-questpie"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "keywords": [
37
+ "questpie",
38
+ "cms",
39
+ "create",
40
+ "scaffold",
41
+ "template"
42
+ ],
43
+ "license": "MIT"
44
+ }