@techstream/quark-create-app 1.2.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 (73) hide show
  1. package/README.md +38 -0
  2. package/package.json +34 -0
  3. package/src/index.js +611 -0
  4. package/templates/base-project/README.md +35 -0
  5. package/templates/base-project/apps/web/next.config.js +6 -0
  6. package/templates/base-project/apps/web/package.json +32 -0
  7. package/templates/base-project/apps/web/postcss.config.mjs +7 -0
  8. package/templates/base-project/apps/web/public/file.svg +1 -0
  9. package/templates/base-project/apps/web/public/globe.svg +1 -0
  10. package/templates/base-project/apps/web/public/next.svg +1 -0
  11. package/templates/base-project/apps/web/public/vercel.svg +1 -0
  12. package/templates/base-project/apps/web/public/window.svg +1 -0
  13. package/templates/base-project/apps/web/src/app/api/auth/[...nextauth]/route.js +4 -0
  14. package/templates/base-project/apps/web/src/app/api/auth/register/route.js +39 -0
  15. package/templates/base-project/apps/web/src/app/api/csrf/route.js +42 -0
  16. package/templates/base-project/apps/web/src/app/api/error-handler.js +21 -0
  17. package/templates/base-project/apps/web/src/app/api/health/route.js +78 -0
  18. package/templates/base-project/apps/web/src/app/api/posts/[id]/route.js +61 -0
  19. package/templates/base-project/apps/web/src/app/api/posts/route.js +34 -0
  20. package/templates/base-project/apps/web/src/app/api/users/[id]/route.js +54 -0
  21. package/templates/base-project/apps/web/src/app/api/users/route.js +36 -0
  22. package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
  23. package/templates/base-project/apps/web/src/app/globals.css +26 -0
  24. package/templates/base-project/apps/web/src/app/layout.js +12 -0
  25. package/templates/base-project/apps/web/src/app/page.js +10 -0
  26. package/templates/base-project/apps/web/src/app/page.test.js +11 -0
  27. package/templates/base-project/apps/web/src/lib/auth-middleware.js +14 -0
  28. package/templates/base-project/apps/web/src/lib/auth.js +102 -0
  29. package/templates/base-project/apps/web/src/middleware.js +265 -0
  30. package/templates/base-project/apps/worker/package.json +28 -0
  31. package/templates/base-project/apps/worker/src/index.js +154 -0
  32. package/templates/base-project/apps/worker/src/index.test.js +19 -0
  33. package/templates/base-project/docker-compose.yml +40 -0
  34. package/templates/base-project/package.json +26 -0
  35. package/templates/base-project/packages/db/package.json +29 -0
  36. package/templates/base-project/packages/db/prisma/migrations/20260202061128_initial/migration.sql +176 -0
  37. package/templates/base-project/packages/db/prisma/migrations/migration_lock.toml +3 -0
  38. package/templates/base-project/packages/db/prisma/schema.prisma +147 -0
  39. package/templates/base-project/packages/db/prisma.config.ts +25 -0
  40. package/templates/base-project/packages/db/scripts/seed.js +47 -0
  41. package/templates/base-project/packages/db/src/client.js +52 -0
  42. package/templates/base-project/packages/db/src/generated/prisma/browser.ts +53 -0
  43. package/templates/base-project/packages/db/src/generated/prisma/client.ts +82 -0
  44. package/templates/base-project/packages/db/src/generated/prisma/commonInputTypes.ts +649 -0
  45. package/templates/base-project/packages/db/src/generated/prisma/enums.ts +19 -0
  46. package/templates/base-project/packages/db/src/generated/prisma/internal/class.ts +305 -0
  47. package/templates/base-project/packages/db/src/generated/prisma/internal/prismaNamespace.ts +1428 -0
  48. package/templates/base-project/packages/db/src/generated/prisma/internal/prismaNamespaceBrowser.ts +217 -0
  49. package/templates/base-project/packages/db/src/generated/prisma/models/Account.ts +2098 -0
  50. package/templates/base-project/packages/db/src/generated/prisma/models/AuditLog.ts +1805 -0
  51. package/templates/base-project/packages/db/src/generated/prisma/models/Job.ts +1737 -0
  52. package/templates/base-project/packages/db/src/generated/prisma/models/Post.ts +1762 -0
  53. package/templates/base-project/packages/db/src/generated/prisma/models/Session.ts +1738 -0
  54. package/templates/base-project/packages/db/src/generated/prisma/models/User.ts +2298 -0
  55. package/templates/base-project/packages/db/src/generated/prisma/models/VerificationToken.ts +1450 -0
  56. package/templates/base-project/packages/db/src/generated/prisma/models.ts +18 -0
  57. package/templates/base-project/packages/db/src/index.js +3 -0
  58. package/templates/base-project/packages/db/src/queries.js +267 -0
  59. package/templates/base-project/packages/db/src/queries.test.js +79 -0
  60. package/templates/base-project/packages/db/src/schemas.js +31 -0
  61. package/templates/base-project/pnpm-workspace.yaml +7 -0
  62. package/templates/base-project/turbo.json +25 -0
  63. package/templates/config/package.json +8 -0
  64. package/templates/config/src/index.js +21 -0
  65. package/templates/jobs/package.json +8 -0
  66. package/templates/jobs/src/definitions.js +9 -0
  67. package/templates/jobs/src/handlers.js +20 -0
  68. package/templates/jobs/src/index.js +2 -0
  69. package/templates/ui/package.json +11 -0
  70. package/templates/ui/src/button.js +19 -0
  71. package/templates/ui/src/card.js +14 -0
  72. package/templates/ui/src/index.js +3 -0
  73. package/templates/ui/src/input.js +11 -0
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # @techstream/quark-create-app CLI
2
+
3
+ Scaffold a new Quark project with sensible defaults for full-stack JavaScript development.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npx @techstream/quark-create-app@latest my-awesome-app
9
+ ```
10
+
11
+ The CLI scaffolds a complete project structure with:
12
+ - **Next.js** web application
13
+ - **Prisma** database schema and migrations
14
+ - **BullMQ** job queues
15
+ - **Docker Compose** setup (PostgreSQL, Redis, Mailhog)
16
+ - **JavaScript** monorepo with `pnpm` workspaces
17
+
18
+ ## Quick Setup
19
+
20
+ ```bash
21
+ cd my-awesome-app
22
+ docker compose up -d
23
+ pnpm db:generate
24
+ pnpm db:migrate
25
+ pnpm dev
26
+ ```
27
+
28
+ ## Common Tasks
29
+
30
+ - **Update Quark packages**: `quark-update` or `pnpm update @techstream/quark-*`
31
+ - **Check for updates**: `quark-update --check`
32
+ - **Configure environment**: Edit `.env` file (see `.env.example`)
33
+
34
+ ## Support
35
+
36
+ For issues, questions, and discussions:
37
+ - šŸ› [Issue Tracker](https://github.com/Bobnoddle/quark/issues)
38
+ - šŸ’¬ [Discussions](https://github.com/Bobnoddle/quark/discussions)
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@techstream/quark-create-app",
3
+ "version": "1.2.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "quark-create-app": "src/index.js",
7
+ "create-quark-app": "src/index.js"
8
+ },
9
+ "files": [
10
+ "src",
11
+ "templates",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "test": "node test-cli.js",
16
+ "test:e2e": "node test-e2e.js",
17
+ "test:integration": "node test-integration.js",
18
+ "test:all": "node test-all.js"
19
+ },
20
+ "dependencies": {
21
+ "chalk": "^5.6.2",
22
+ "commander": "^12.1.0",
23
+ "execa": "^9.6.1",
24
+ "fs-extra": "^11.3.3",
25
+ "prompts": "^2.4.2"
26
+ },
27
+ "publishConfig": {
28
+ "registry": "https://registry.npmjs.org",
29
+ "access": "public"
30
+ },
31
+ "engines": {
32
+ "node": ">=24"
33
+ }
34
+ }
package/src/index.js ADDED
@@ -0,0 +1,611 @@
1
+ #!/usr/bin/env node
2
+ import crypto from "node:crypto";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import chalk from "chalk";
6
+ import { Command } from "commander";
7
+ import { execa } from "execa";
8
+ import fs from "fs-extra";
9
+ import prompts from "prompts";
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ const templatesDir = path.join(__dirname, "../templates");
13
+ const pkg = await fs.readJSON(path.join(__dirname, "../package.json"));
14
+
15
+ const program = new Command();
16
+
17
+ program
18
+ .name("quark-create-app")
19
+ .description("Scaffold a new project from the Quark monorepo")
20
+ .version(pkg.version);
21
+
22
+ /**
23
+ * Generate a cryptographically secure random string
24
+ * @param {number} length - Length of the random string (default: 32)
25
+ * @returns {string} Base64 encoded random string
26
+ */
27
+ function generateSecureSecret(length = 32) {
28
+ return crypto
29
+ .randomBytes(length)
30
+ .toString("base64")
31
+ .replace(/[/+=]/g, "")
32
+ .substring(0, length);
33
+ }
34
+
35
+ /**
36
+ * Generate a secure random password
37
+ * @param {number} length - Length of the password (default: 24)
38
+ * @returns {string} Alphanumeric password
39
+ */
40
+ function generateSecurePassword(length = 24) {
41
+ const chars = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789";
42
+ const bytes = crypto.randomBytes(length);
43
+ let result = "";
44
+ for (let i = 0; i < length; i++) {
45
+ result += chars[bytes[i] % chars.length];
46
+ }
47
+ return result;
48
+ }
49
+
50
+ /**
51
+ * Copy a template directory to the target location, with variable substitution
52
+ */
53
+ async function copyTemplate(templateName, targetDir, variables = {}) {
54
+ const templatePath = path.join(templatesDir, templateName);
55
+
56
+ if (!(await fs.pathExists(templatePath))) {
57
+ throw new Error(`Template not found: ${templateName}`);
58
+ }
59
+
60
+ // Copy the template
61
+ await fs.copy(templatePath, targetDir);
62
+
63
+ // Replace variables in package.json files
64
+ if (Object.keys(variables).length > 0) {
65
+ const packageJsonPath = path.join(targetDir, "package.json");
66
+ if (await fs.pathExists(packageJsonPath)) {
67
+ let content = await fs.readFile(packageJsonPath, "utf-8");
68
+
69
+ for (const [key, value] of Object.entries(variables)) {
70
+ const pattern = new RegExp(key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
71
+ content = content.replace(pattern, value);
72
+ }
73
+
74
+ await fs.writeFile(packageJsonPath, content);
75
+ }
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Initialize a git repository and create an initial commit
81
+ */
82
+ async function initializeGit(projectDir) {
83
+ try {
84
+ // Initialize git repo
85
+ await execa("git", ["init"], { cwd: projectDir });
86
+
87
+ // Add all files
88
+ await execa("git", ["add", "."], { cwd: projectDir });
89
+
90
+ // Create initial commit
91
+ await execa(
92
+ "git",
93
+ ["commit", "-m", "Initial commit: Quark project scaffold"],
94
+ {
95
+ cwd: projectDir,
96
+ },
97
+ );
98
+
99
+ return true;
100
+ } catch (error) {
101
+ console.warn(
102
+ chalk.yellow(`āš ļø Git initialization failed: ${error.message}`),
103
+ );
104
+ return false;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Update package.json name with scope
110
+ */
111
+ async function updatePackageJsonName(filePath, scope) {
112
+ const content = await fs.readFile(filePath, "utf-8");
113
+ const packageJson = JSON.parse(content);
114
+
115
+ // Extract package name (e.g., "@myquark/ui" -> "ui")
116
+ const parts = packageJson.name.split("/");
117
+ const packageName = parts[parts.length - 1];
118
+
119
+ packageJson.name = `@${scope}/${packageName}`;
120
+
121
+ await fs.writeFile(filePath, `${JSON.stringify(packageJson, null, 2)}\n`);
122
+ }
123
+
124
+ /**
125
+ * Replace @techstream/quark-* workspace deps with @scope/* for local packages.
126
+ * Also removes deps for packages that were not selected.
127
+ */
128
+ function replaceDepsScope(deps, scope, selectedPackages) {
129
+ if (!deps) return;
130
+ for (const [key, value] of Object.entries(deps)) {
131
+ if (key.startsWith("@techstream/quark-") && value === "workspace:*") {
132
+ const packageName = key.replace("@techstream/quark-", "");
133
+ delete deps[key];
134
+ // Only keep the dep if the package was selected (or is "db" which is always required)
135
+ if (packageName === "db" || selectedPackages.includes(packageName)) {
136
+ deps[`@${scope}/${packageName}`] = value;
137
+ }
138
+ }
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Replace @techstream/quark-* import paths in all .js source files
144
+ * for workspace packages (db, jobs, ui, config) with @scope/* equivalents.
145
+ * Registry packages (@techstream/quark-core) are left untouched.
146
+ */
147
+ async function replaceImportsInSourceFiles(dir, scope) {
148
+ const workspacePackages = ["db", "jobs", "ui", "config"];
149
+ const entries = await fs.readdir(dir, { withFileTypes: true });
150
+
151
+ for (const entry of entries) {
152
+ const fullPath = path.join(dir, entry.name);
153
+
154
+ if (entry.isDirectory() && entry.name !== "node_modules" && entry.name !== ".next") {
155
+ await replaceImportsInSourceFiles(fullPath, scope);
156
+ } else if (entry.isFile() && /\.(js|ts|jsx|tsx|mjs)$/.test(entry.name)) {
157
+ let content = await fs.readFile(fullPath, "utf-8");
158
+ let changed = false;
159
+
160
+ for (const pkg of workspacePackages) {
161
+ const pattern = new RegExp(`@techstream/quark-${pkg}`, "g");
162
+ if (pattern.test(content)) {
163
+ content = content.replace(pattern, `@${scope}/${pkg}`);
164
+ changed = true;
165
+ }
166
+ }
167
+
168
+ if (changed) {
169
+ await fs.writeFile(fullPath, content);
170
+ }
171
+ }
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Validate project name to prevent path traversal and ensure safe directory creation
177
+ */
178
+ function validateProjectName(name) {
179
+ if (!name || typeof name !== "string") {
180
+ throw new Error("Project name is required");
181
+ }
182
+ // Only allow safe characters: alphanumeric, hyphens, underscores, dots
183
+ if (!/^[a-zA-Z0-9._-]+$/.test(name)) {
184
+ throw new Error(
185
+ "Project name may only contain letters, numbers, hyphens, underscores, and dots",
186
+ );
187
+ }
188
+ // Block path traversal patterns
189
+ if (name.startsWith(".") || name.includes("..")) {
190
+ throw new Error("Project name must not start with '.' or contain '..'");
191
+ }
192
+
193
+ const resolved = path.resolve(process.cwd(), name);
194
+ if (!resolved.startsWith(process.cwd())) {
195
+ throw new Error("Project name must not escape the current directory");
196
+ }
197
+
198
+ return resolved;
199
+ }
200
+
201
+ program
202
+ .argument("<project-name>", "Name of the project to create")
203
+ .action(async (projectName) => {
204
+ console.log(
205
+ chalk.blue.bold(`\n\uD83D\uDE80 Creating your new Quark project: ${projectName}\n`),
206
+ );
207
+
208
+ const targetDir = validateProjectName(projectName);
209
+ const scope = projectName.toLowerCase().replace(/[^a-z0-9-]/g, "");
210
+
211
+ // Check if directory already exists
212
+ if (await fs.pathExists(targetDir)) {
213
+ console.error(chalk.red(`āœ— Directory already exists: ${targetDir}`));
214
+ process.exit(1);
215
+ }
216
+
217
+ try {
218
+ // Create the base directory
219
+ await fs.ensureDir(targetDir);
220
+
221
+ // Step 1: Copy base project template
222
+ console.log(chalk.cyan(" šŸ“¦ Scaffolding base project structure..."));
223
+ await copyTemplate("base-project", targetDir, {
224
+ "@myquark": `@${scope}`,
225
+ });
226
+
227
+ // Step 2: Create apps directory
228
+ await fs.ensureDir(path.join(targetDir, "apps"));
229
+
230
+ // Step 3: Create packages directory
231
+ await fs.ensureDir(path.join(targetDir, "packages"));
232
+
233
+ // Step 4: Copy required packages (always included)
234
+ console.log(chalk.cyan("\n šŸ“¦ Setting up required packages..."));
235
+
236
+ // Database package is always required (already copied from base-project)
237
+ // Just update its package.json name
238
+ const dbPackageDir = path.join(targetDir, "packages", "db");
239
+ const dbPackageJsonPath = path.join(dbPackageDir, "package.json");
240
+ const dbPackageJson = await fs.readJSON(dbPackageJsonPath);
241
+ dbPackageJson.name = `@${scope}/db`;
242
+ await fs.writeFile(
243
+ dbPackageJsonPath,
244
+ `${JSON.stringify(dbPackageJson, null, 2)}\n`,
245
+ );
246
+ console.log(chalk.green(` āœ“ db (required)`));
247
+
248
+ // Step 5: Ask which optional features to eject
249
+ console.log(chalk.cyan("\n šŸŽÆ Configuring optional features..."));
250
+ const response = await prompts([
251
+ {
252
+ type: "multiselect",
253
+ name: "features",
254
+ message: "Which optional packages would you like to include?",
255
+ choices: [
256
+ {
257
+ title: "UI Components (packages/ui)",
258
+ value: "ui",
259
+ selected: true,
260
+ },
261
+ {
262
+ title: "Job Definitions (packages/jobs)",
263
+ value: "jobs",
264
+ selected: true,
265
+ },
266
+ {
267
+ title: "Configuration (packages/config)",
268
+ value: "config",
269
+ selected: false,
270
+ },
271
+ ],
272
+ },
273
+ ]);
274
+
275
+ const { features } = response;
276
+
277
+ // Handle prompt cancellation (Ctrl+C)
278
+ if (!features) {
279
+ console.log(chalk.yellow("\n\u26A0\uFE0F Setup cancelled."));
280
+ await fs.remove(targetDir);
281
+ process.exit(0);
282
+ }
283
+
284
+ // Step 6: Copy selected optional packages
285
+ if (features.length > 0) {
286
+ console.log(chalk.cyan("\n šŸ“‹ Setting up optional packages..."));
287
+
288
+ for (const feature of features) {
289
+ const packageDir = path.join(targetDir, "packages", feature);
290
+ await fs.ensureDir(packageDir);
291
+ await copyTemplate(feature, packageDir);
292
+
293
+ // Update package.json with proper scope
294
+ const packageJsonPath = path.join(packageDir, "package.json");
295
+ await updatePackageJsonName(packageJsonPath, scope);
296
+
297
+ console.log(chalk.green(` āœ“ ${feature}`));
298
+ }
299
+ }
300
+
301
+ // Step 7: Update app dependencies to use correct scope
302
+ console.log(chalk.cyan("\n šŸ”§ Updating app dependencies..."));
303
+
304
+ // Update app package.json files to use correct scope
305
+ const appPaths = [
306
+ path.join(targetDir, "apps", "web", "package.json"),
307
+ path.join(targetDir, "apps", "worker", "package.json"),
308
+ ];
309
+
310
+ for (const appPkgPath of appPaths) {
311
+ if (await fs.pathExists(appPkgPath)) {
312
+ const appPkg = await fs.readJSON(appPkgPath);
313
+ replaceDepsScope(appPkg.dependencies, scope, features);
314
+ replaceDepsScope(appPkg.devDependencies, scope, features);
315
+ await fs.writeFile(
316
+ appPkgPath,
317
+ `${JSON.stringify(appPkg, null, 2)}\n`,
318
+ );
319
+ }
320
+ }
321
+
322
+ console.log(chalk.green(` āœ“ App dependencies updated`));
323
+
324
+ // Step 7b: Replace workspace package imports in source files
325
+ console.log(chalk.cyan("\n šŸ”„ Updating import paths..."));
326
+ await replaceImportsInSourceFiles(targetDir, scope);
327
+ console.log(chalk.green(` āœ“ Import paths updated`));
328
+
329
+ // Step 8: Create .env.example file
330
+ console.log(chalk.cyan("\n šŸ“‹ Creating environment configuration..."));
331
+ const envExampleTemplate = `# --- Database Configuration ---
332
+ # These map to the service names in docker-compose.yml
333
+ # āš ļø SECURITY WARNING: Change these default passwords in production!
334
+ # Generate strong passwords with: openssl rand -base64 32
335
+ POSTGRES_HOST=localhost
336
+ POSTGRES_PORT=5432
337
+ POSTGRES_USER=quark_user
338
+ POSTGRES_PASSWORD=CHANGE_ME_TO_STRONG_PASSWORD
339
+ POSTGRES_DB=${scope}_dev
340
+ # Optional: Set DATABASE_URL to override the dynamic construction above
341
+ # DATABASE_URL="postgresql://quark_user:CHANGE_ME_TO_STRONG_PASSWORD@localhost:5432/${scope}_dev?schema=public"
342
+
343
+ # --- Redis Configuration ---
344
+ REDIS_HOST=localhost
345
+ REDIS_PORT=6379
346
+ # Optional: Set REDIS_URL to override the dynamic construction above
347
+ # REDIS_URL="redis://localhost:6379"
348
+
349
+ # --- Mailhog Configuration ---
350
+ MAILHOG_HOST=localhost
351
+ MAILHOG_SMTP_PORT=1025
352
+ MAILHOG_UI_PORT=8025
353
+ # Optional: Set MAILHOG_SMTP_URL to override the dynamic construction above
354
+ # MAILHOG_SMTP_URL="smtp://localhost:1025"
355
+
356
+ # --- Application URL ---
357
+ # The canonical URL of your application.
358
+ # NEXTAUTH_URL, CORS origins, and other URL-dependent settings are derived from this.
359
+ # Development: http://localhost:3000
360
+ # Production: https://yourdomain.com
361
+ APP_URL=http://localhost:3000
362
+
363
+ # --- NextAuth Configuration ---
364
+ # āš ļø CRITICAL: Generate a secure secret with: openssl rand -base64 32
365
+ # This secret is used to encrypt JWT tokens and session data
366
+ NEXTAUTH_SECRET=CHANGE_ME_TO_STRONG_SECRET
367
+
368
+ # --- OAuth Providers (Optional) ---
369
+ # GitHub OAuth - Get credentials at: https://github.com/settings/developers
370
+ # GITHUB_ID=your_github_client_id
371
+ # GITHUB_SECRET=your_github_client_secret
372
+
373
+ # Google OAuth - Get credentials at: https://console.cloud.google.com/apis/credentials
374
+ # GOOGLE_CLIENT_ID=your_google_client_id
375
+ # GOOGLE_CLIENT_SECRET=your_google_client_secret
376
+
377
+ # --- Web App Configuration ---
378
+ WEB_PORT=3000
379
+
380
+ # --- Worker Configuration ---
381
+ WORKER_CONCURRENCY=5
382
+ `;
383
+ await fs.writeFile(
384
+ path.join(targetDir, ".env.example"),
385
+ envExampleTemplate,
386
+ );
387
+ console.log(chalk.green(` āœ“ .env.example`));
388
+
389
+ // Step 9: Generate .env with secure defaults
390
+ console.log(chalk.cyan("\n šŸ”‘ Generating secure environment file..."));
391
+
392
+ // Generate secure random values
393
+ const dbPassword = generateSecurePassword(24);
394
+ const nextAuthSecret = generateSecureSecret(32);
395
+
396
+ // Create .env with auto-generated secure values
397
+ const envContent = `# --- Database Configuration ---
398
+ POSTGRES_HOST=localhost
399
+ POSTGRES_PORT=5432
400
+ POSTGRES_USER=quark_user
401
+ POSTGRES_PASSWORD=${dbPassword}
402
+ POSTGRES_DB=${scope}_dev
403
+
404
+ # --- Redis Configuration ---
405
+ REDIS_HOST=localhost
406
+ REDIS_PORT=6379
407
+
408
+ # --- Mailhog Configuration ---
409
+ MAILHOG_HOST=localhost
410
+ MAILHOG_SMTP_PORT=1025
411
+ MAILHOG_UI_PORT=8025
412
+
413
+ # --- NextAuth Configuration ---
414
+ NEXTAUTH_SECRET=${nextAuthSecret}
415
+
416
+ # --- Application URL ---
417
+ APP_URL=http://localhost:3000
418
+
419
+ # --- Web App Configuration ---
420
+ WEB_PORT=3000
421
+
422
+ # --- Worker Configuration ---
423
+ WORKER_CONCURRENCY=5
424
+ `;
425
+ await fs.writeFile(path.join(targetDir, ".env"), envContent);
426
+ console.log(
427
+ chalk.green(` āœ“ .env (with auto-generated secure secrets)`),
428
+ );
429
+
430
+ // Step 11: Create .quark-link.json to track Quark version
431
+ const quarkLinkJson = {
432
+ quarkVersion: process.env.QUARK_VERSION || "latest",
433
+ quarkSourcePath: process.env.QUARK_SOURCE_PATH || "../../quark",
434
+ scaffoldedDate: new Date().toISOString(),
435
+ requiredPackages: ["db"],
436
+ packages: features,
437
+ };
438
+ await fs.writeFile(
439
+ path.join(targetDir, ".quark-link.json"),
440
+ JSON.stringify(quarkLinkJson, null, 2),
441
+ );
442
+ console.log(chalk.green(` āœ“ .quark-link.json`));
443
+
444
+ // Step 11: Initialize git repository
445
+ console.log(chalk.cyan("\n šŸ“ Initializing git repository..."));
446
+ const gitInitialized = await initializeGit(targetDir);
447
+ if (gitInitialized) {
448
+ console.log(chalk.green(` āœ“ Git initialized with initial commit`));
449
+ }
450
+
451
+ // Step 12: Run pnpm install
452
+ console.log(chalk.cyan("\n šŸ“¦ Installing dependencies..."));
453
+ try {
454
+ await execa("pnpm", ["install"], {
455
+ cwd: targetDir,
456
+ stdio: "inherit",
457
+ });
458
+ console.log(chalk.green(`\n āœ“ Dependencies installed`));
459
+ } catch (installError) {
460
+ console.warn(
461
+ chalk.yellow(
462
+ `\n āš ļø pnpm install failed: ${installError.message}`,
463
+ ),
464
+ );
465
+ console.warn(
466
+ chalk.yellow(
467
+ ` Run 'pnpm install' manually after resolving the issue.`,
468
+ ),
469
+ );
470
+ }
471
+
472
+ // Success message
473
+ console.log(
474
+ chalk.green.bold(
475
+ `\nāœ… Project "${projectName}" created successfully!\n`,
476
+ ),
477
+ );
478
+
479
+ console.log(chalk.white(`šŸ“‚ Project location: ${targetDir}\n`));
480
+ console.log(chalk.cyan("Next steps:"));
481
+ console.log(chalk.white(` 1. cd ${projectName}`));
482
+ console.log(chalk.white(` 2. docker compose up -d`));
483
+ console.log(chalk.white(` 3. pnpm dev\n`));
484
+
485
+ console.log(chalk.cyan("Important:"));
486
+ console.log(
487
+ chalk.white(
488
+ ` • Update Quark core with: pnpm update @techstream/quark-core`,
489
+ ),
490
+ );
491
+ console.log(
492
+ chalk.white(` • Use 'quark-update' to upgrade Quark packages\n`),
493
+ );
494
+
495
+ console.log(chalk.cyan("Learn more:"));
496
+ console.log(chalk.white(` šŸ“– Docs: https://github.com/Bobnoddle/quark`));
497
+ console.log(
498
+ chalk.white(` šŸ’¬ Issues: https://github.com/Bobnoddle/quark/issues\n`),
499
+ );
500
+ } catch (error) {
501
+ console.error(chalk.red(`\nāœ— Error creating project: ${error.message}`));
502
+ console.error(chalk.dim(error.stack));
503
+
504
+ // Clean up on error
505
+ if (await fs.pathExists(targetDir)) {
506
+ await fs.remove(targetDir);
507
+ }
508
+
509
+ process.exit(1);
510
+ }
511
+ });
512
+
513
+ /**
514
+ * quark-update command
515
+ * Updates Quark core infrastructure in a scaffolded project
516
+ */
517
+ program
518
+ .command("update")
519
+ .description("Update Quark core infrastructure in the current project")
520
+ .option("--check", "Check for updates without applying")
521
+ .option("--force", "Skip safety checks")
522
+ .action(async (options) => {
523
+ console.log(chalk.blue.bold(`\nšŸ”„ Quark Package Update\n`));
524
+
525
+ // Check if .quark-link.json exists
526
+ const quarkLinkPath = path.join(process.cwd(), ".quark-link.json");
527
+ if (!(await fs.pathExists(quarkLinkPath))) {
528
+ console.error(
529
+ chalk.red("āœ— .quark-link.json not found. Are you in a Quark project?"),
530
+ );
531
+ process.exit(1);
532
+ }
533
+
534
+ const quarkLink = await fs.readJSON(quarkLinkPath);
535
+ console.log(chalk.cyan(`Current Quark version: ${quarkLink.quarkVersion}`));
536
+ console.log(chalk.cyan(`Scaffolded: ${quarkLink.scaffoldedDate}\n`));
537
+
538
+ if (options.check) {
539
+ console.log(chalk.yellow("Checking for updates..."));
540
+ console.log(
541
+ chalk.white("Run 'pnpm update @techstream/quark-*' to apply updates."),
542
+ );
543
+ return;
544
+ }
545
+
546
+ try {
547
+ // Warn if git has uncommitted changes
548
+ if (!options.force) {
549
+ console.log(chalk.yellow("āš ļø Checking for uncommitted changes..."));
550
+ try {
551
+ await execa("git", ["diff", "--exit-code"], {
552
+ cwd: process.cwd(),
553
+ });
554
+ } catch {
555
+ console.log(
556
+ chalk.yellow(
557
+ "āš ļø You have uncommitted changes. Commit or stash them first.",
558
+ ),
559
+ );
560
+ console.log(
561
+ chalk.white("Use --force to skip this check (not recommended).\n"),
562
+ );
563
+ process.exit(1);
564
+ }
565
+ }
566
+
567
+ // Run pnpm update
568
+ console.log(chalk.cyan("\nšŸ“¦ Updating Quark core infrastructure...\n"));
569
+ await execa("pnpm", ["update", "@techstream/quark-core"], {
570
+ cwd: process.cwd(),
571
+ stdio: "inherit",
572
+ });
573
+
574
+ // Update .quark-link.json
575
+ let updatedVersion = "updated";
576
+ try {
577
+ const corePkg = await fs.readJSON(
578
+ path.join(
579
+ process.cwd(),
580
+ "node_modules",
581
+ "@techstream",
582
+ "quark-core",
583
+ "package.json",
584
+ ),
585
+ );
586
+ updatedVersion = corePkg.version;
587
+ } catch {}
588
+ quarkLink.quarkVersion = updatedVersion;
589
+ quarkLink.updatedDate = new Date().toISOString();
590
+ await fs.writeFile(quarkLinkPath, JSON.stringify(quarkLink, null, 2));
591
+
592
+ console.log(
593
+ chalk.green("\nāœ… Quark core infrastructure updated successfully!\n"),
594
+ );
595
+ console.log(
596
+ chalk.cyan("Note: This updates @techstream/quark-core only.\n"),
597
+ );
598
+ console.log(chalk.cyan("Next steps:"));
599
+ console.log(chalk.white(` 1. pnpm install (if prompted)`));
600
+ console.log(chalk.white(` 2. pnpm lint`));
601
+ console.log(chalk.white(` 3. pnpm test`));
602
+ console.log(
603
+ chalk.white(` 4. git add . && git commit -m "chore: update Quark"\n`),
604
+ );
605
+ } catch (error) {
606
+ console.error(chalk.red(`\nāœ— Update failed: ${error.message}\n`));
607
+ process.exit(1);
608
+ }
609
+ });
610
+
611
+ program.parse();