@msishamim/create-next-monorepo 1.0.0 → 2.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.
package/dist/cli.js CHANGED
@@ -43,6 +43,16 @@ var ProjectConfig = class {
43
43
  packageManager;
44
44
  gitInit;
45
45
  githubFiles;
46
+ docker;
47
+ i18n;
48
+ payments;
49
+ email;
50
+ apiDocs;
51
+ storage;
52
+ e2e;
53
+ storybook;
54
+ cache;
55
+ logging;
46
56
  /** Resolved npm package versions — set by Generator before template rendering */
47
57
  versions = {};
48
58
  constructor(options) {
@@ -58,6 +68,16 @@ var ProjectConfig = class {
58
68
  this.packageManager = options.packageManager ?? "pnpm";
59
69
  this.gitInit = options.gitInit ?? true;
60
70
  this.githubFiles = options.githubFiles ?? false;
71
+ this.docker = options.docker ?? "none";
72
+ this.i18n = options.i18n ?? "none";
73
+ this.payments = options.payments ?? "none";
74
+ this.email = options.email ?? "none";
75
+ this.apiDocs = options.apiDocs ?? "none";
76
+ this.storage = options.storage ?? "none";
77
+ this.e2e = options.e2e ?? "none";
78
+ this.storybook = options.storybook ?? false;
79
+ this.cache = options.cache ?? "none";
80
+ this.logging = options.logging ?? "default";
61
81
  }
62
82
  // ── Derived name variants ──────────────────────────────────────
63
83
  /** PascalCase (e.g. "my-app" → "MyApp") */
@@ -86,6 +106,33 @@ var ProjectConfig = class {
86
106
  get usesTailwind() {
87
107
  return this.styling === "tailwind";
88
108
  }
109
+ get hasDocker() {
110
+ return this.docker !== "none";
111
+ }
112
+ get hasI18n() {
113
+ return this.i18n !== "none";
114
+ }
115
+ get hasPayments() {
116
+ return this.payments !== "none";
117
+ }
118
+ get hasEmail() {
119
+ return this.email !== "none";
120
+ }
121
+ get hasApiDocs() {
122
+ return this.apiDocs !== "none";
123
+ }
124
+ get hasStorage() {
125
+ return this.storage !== "none";
126
+ }
127
+ get hasE2e() {
128
+ return this.e2e !== "none";
129
+ }
130
+ get hasCache() {
131
+ return this.cache !== "none";
132
+ }
133
+ get hasLogging() {
134
+ return this.logging !== "default";
135
+ }
89
136
  /** Install command for the selected package manager */
90
137
  get installCommand() {
91
138
  switch (this.packageManager) {
@@ -172,6 +219,32 @@ var ProjectConfig = class {
172
219
  packages.push("jest", "@types/jest", "ts-jest");
173
220
  }
174
221
  packages.push("zod");
222
+ if (this.i18n === "next-intl") packages.push("next-intl");
223
+ if (this.payments === "stripe") packages.push("stripe");
224
+ if (this.payments === "lemonsqueezy") packages.push("@lemonsqueezy/lemonsqueezy.js");
225
+ if (this.payments === "paddle") packages.push("@paddle/paddle-node-sdk");
226
+ if (this.email === "resend") packages.push("resend", "@react-email/components");
227
+ if (this.email === "nodemailer") packages.push("nodemailer", "@types/nodemailer");
228
+ if (this.email === "sendgrid") packages.push("@sendgrid/mail");
229
+ if (this.apiDocs === "swagger" || this.apiDocs === "redoc") {
230
+ if (this.backend === "nestjs") {
231
+ packages.push("@nestjs/swagger");
232
+ } else {
233
+ packages.push("swagger-jsdoc", "swagger-ui-express", "@types/swagger-jsdoc", "@types/swagger-ui-express");
234
+ }
235
+ }
236
+ if (this.apiDocs === "redoc") packages.push("redoc-express");
237
+ if (this.storage === "s3") packages.push("@aws-sdk/client-s3", "@aws-sdk/s3-request-presigner");
238
+ if (this.storage === "uploadthing") packages.push("uploadthing", "@uploadthing/react");
239
+ if (this.storage === "cloudinary") packages.push("cloudinary");
240
+ if (this.e2e === "playwright") packages.push("@playwright/test");
241
+ if (this.e2e === "cypress") packages.push("cypress");
242
+ if (this.storybook) {
243
+ packages.push("storybook", "@storybook/react", "@storybook/react-vite", "@storybook/addon-essentials");
244
+ }
245
+ if (this.cache === "redis") packages.push("ioredis");
246
+ if (this.logging === "pino") packages.push("pino", "pino-pretty");
247
+ if (this.logging === "winston") packages.push("winston");
175
248
  return packages;
176
249
  }
177
250
  };
@@ -246,7 +319,47 @@ var FALLBACK_VERSIONS = {
246
319
  "@types/jest": "^29.5.14",
247
320
  "ts-jest": "^29.3.2",
248
321
  // Validation
249
- zod: "^3.24.3"
322
+ zod: "^3.24.3",
323
+ // ── v2.0 options ──
324
+ // i18n
325
+ "next-intl": "^4.1.0",
326
+ // Payments
327
+ stripe: "^17.7.0",
328
+ "@lemonsqueezy/lemonsqueezy.js": "^4.0.0",
329
+ "@paddle/paddle-node-sdk": "^1.7.0",
330
+ // Email
331
+ resend: "^4.1.0",
332
+ "@react-email/components": "^0.0.36",
333
+ nodemailer: "^6.10.0",
334
+ "@types/nodemailer": "^6.4.17",
335
+ "@sendgrid/mail": "^8.1.0",
336
+ // API Docs
337
+ "@nestjs/swagger": "^8.1.0",
338
+ "swagger-jsdoc": "^6.2.8",
339
+ "swagger-ui-express": "^5.0.1",
340
+ "@types/swagger-jsdoc": "^6.0.4",
341
+ "@types/swagger-ui-express": "^4.1.8",
342
+ "redoc-express": "^2.1.0",
343
+ // Storage
344
+ "@aws-sdk/client-s3": "^3.750.0",
345
+ "@aws-sdk/s3-request-presigner": "^3.750.0",
346
+ uploadthing: "^7.6.0",
347
+ "@uploadthing/react": "^7.3.0",
348
+ cloudinary: "^2.6.0",
349
+ // E2E
350
+ "@playwright/test": "^1.50.0",
351
+ cypress: "^14.0.0",
352
+ // Storybook
353
+ storybook: "^8.5.0",
354
+ "@storybook/react": "^8.5.0",
355
+ "@storybook/react-vite": "^8.5.0",
356
+ "@storybook/addon-essentials": "^8.5.0",
357
+ // Cache
358
+ ioredis: "^5.6.0",
359
+ // Logging
360
+ pino: "^9.6.0",
361
+ "pino-pretty": "^13.0.0",
362
+ winston: "^3.17.0"
250
363
  };
251
364
  var VersionResolver = class {
252
365
  resolved = {};
@@ -436,11 +549,27 @@ function getExpectedDirectories(config) {
436
549
  dirs.push(stateDir);
437
550
  }
438
551
  if (config.githubFiles) {
439
- dirs.push(
440
- ".github/ISSUE_TEMPLATE",
441
- ".github/workflows"
442
- );
443
- }
552
+ dirs.push(".github/ISSUE_TEMPLATE", ".github/workflows");
553
+ }
554
+ if (config.docker === "full") dirs.push("nginx");
555
+ if (config.hasI18n) dirs.push("apps/web/i18n", "apps/web/messages", "apps/web/app/[locale]");
556
+ if (config.hasPayments) {
557
+ dirs.push("packages/payments/src");
558
+ dirs.push("apps/web/app/pricing");
559
+ if (config.backend === "nestjs") dirs.push("apps/api/src/payments");
560
+ }
561
+ if (config.hasEmail) dirs.push("packages/email/src", "packages/email/src/templates");
562
+ if (config.hasApiDocs) dirs.push("apps/api/src/docs");
563
+ if (config.hasStorage) {
564
+ dirs.push("packages/storage/src");
565
+ if (config.storage === "uploadthing") dirs.push("apps/web/app/api/uploadthing");
566
+ }
567
+ if (config.hasE2e) {
568
+ dirs.push(config.e2e === "playwright" ? "apps/web/e2e" : "apps/web/cypress/e2e");
569
+ }
570
+ if (config.storybook) dirs.push("packages/ui/.storybook");
571
+ if (config.hasCache) dirs.push("packages/cache/src");
572
+ if (config.hasLogging) dirs.push("packages/lib/src/logger");
444
573
  return dirs;
445
574
  }
446
575
  function getExpectedFiles(config) {
@@ -601,6 +730,94 @@ function getExpectedFiles(config) {
601
730
  ".github/workflows/ci.yml"
602
731
  );
603
732
  }
733
+ if (config.hasDocker) {
734
+ files.push("apps/web/Dockerfile", "apps/api/Dockerfile", "docker-compose.prod.yml", ".dockerignore");
735
+ if (config.docker === "full") files.push("nginx/nginx.conf", "nginx/Dockerfile");
736
+ }
737
+ if (config.hasI18n) {
738
+ files.push(
739
+ "apps/web/i18n/request.ts",
740
+ "apps/web/i18n/routing.ts",
741
+ "apps/web/i18n/navigation.ts",
742
+ "apps/web/messages/en.json",
743
+ "apps/web/messages/ar.json",
744
+ "apps/web/app/[locale]/layout.tsx",
745
+ "apps/web/app/[locale]/page.tsx",
746
+ "apps/web/components/language-switcher.tsx"
747
+ );
748
+ }
749
+ if (config.hasPayments) {
750
+ files.push(
751
+ "packages/payments/package.json",
752
+ "packages/payments/tsconfig.json",
753
+ "packages/payments/src/index.ts",
754
+ "packages/payments/src/client.ts",
755
+ "packages/payments/src/webhook.ts",
756
+ "packages/payments/src/checkout.ts",
757
+ "packages/payments/src/subscription.ts",
758
+ "apps/web/app/pricing/page.tsx"
759
+ );
760
+ if (config.backend === "nestjs") {
761
+ files.push("apps/api/src/payments/payments.controller.ts");
762
+ } else {
763
+ files.push("apps/api/src/routes/payments.ts");
764
+ }
765
+ }
766
+ if (config.hasEmail) {
767
+ files.push(
768
+ "packages/email/package.json",
769
+ "packages/email/tsconfig.json",
770
+ "packages/email/src/index.ts",
771
+ "packages/email/src/client.ts",
772
+ "packages/email/src/send.ts",
773
+ "packages/email/src/templates/welcome.tsx",
774
+ "packages/email/src/templates/reset-password.tsx"
775
+ );
776
+ }
777
+ if (config.hasApiDocs) {
778
+ files.push("apps/api/src/docs/swagger-config.ts", "apps/api/src/docs/setup.ts");
779
+ }
780
+ if (config.hasStorage) {
781
+ files.push(
782
+ "packages/storage/package.json",
783
+ "packages/storage/tsconfig.json",
784
+ "packages/storage/src/index.ts",
785
+ "packages/storage/src/client.ts",
786
+ "packages/storage/src/upload.ts",
787
+ "packages/storage/src/download.ts"
788
+ );
789
+ if (config.storage === "uploadthing") {
790
+ files.push("apps/web/app/api/uploadthing/core.ts", "apps/web/app/api/uploadthing/route.ts");
791
+ }
792
+ }
793
+ if (config.hasE2e) {
794
+ if (config.e2e === "playwright") {
795
+ files.push("apps/web/playwright.config.ts", "apps/web/e2e/example.spec.ts");
796
+ } else {
797
+ files.push("apps/web/cypress.config.ts", "apps/web/cypress/e2e/example.cy.ts");
798
+ }
799
+ }
800
+ if (config.storybook) {
801
+ files.push(
802
+ "packages/ui/.storybook/main.ts",
803
+ "packages/ui/.storybook/preview.ts",
804
+ "packages/ui/src/components/button.stories.tsx",
805
+ "packages/ui/src/components/card.stories.tsx",
806
+ "packages/ui/src/components/input.stories.tsx"
807
+ );
808
+ }
809
+ if (config.hasCache) {
810
+ files.push(
811
+ "packages/cache/package.json",
812
+ "packages/cache/tsconfig.json",
813
+ "packages/cache/src/index.ts",
814
+ "packages/cache/src/client.ts",
815
+ "packages/cache/src/service.ts"
816
+ );
817
+ }
818
+ if (config.hasLogging) {
819
+ files.push("packages/lib/src/logger/index.ts", "packages/lib/src/logger/logger.ts");
820
+ }
604
821
  return files;
605
822
  }
606
823
 
@@ -3127,287 +3344,511 @@ ${setupPm}
3127
3344
  `;
3128
3345
  }
3129
3346
 
3130
- // src/templates/strategies/nestjs-templates.ts
3131
- var NestjsTemplateStrategy = class {
3132
- packageJson(config) {
3133
- return `{
3134
- "name": "@${config.name}/api",
3135
- "version": "0.0.0",
3347
+ // src/templates/cache-templates.ts
3348
+ function cachePackageJson(config) {
3349
+ return `{
3350
+ "name": "@${config.name}/cache",
3351
+ "version": "1.0.0",
3136
3352
  "private": true,
3137
- "scripts": {
3138
- "build": "nest build",
3139
- "dev": "nest start --watch",
3140
- "start": "nest start",
3141
- "start:prod": "node dist/main",
3142
- "lint": "eslint src --ext .ts",
3143
- "test": "${config.testing === "vitest" ? "vitest run" : "jest"}"
3353
+ "type": "module",
3354
+ "main": "./src/index.ts",
3355
+ "types": "./src/index.ts",
3356
+ "exports": {
3357
+ ".": "./src/index.ts",
3358
+ "./client": "./src/client.ts",
3359
+ "./service": "./src/service.ts"
3144
3360
  },
3145
3361
  "dependencies": {
3146
- "@nestjs/common": "${config.versions["@nestjs/common"] ?? "^11.0.20"}",
3147
- "@nestjs/core": "${config.versions["@nestjs/core"] ?? "^11.0.20"}",
3148
- "@nestjs/platform-express": "${config.versions["@nestjs/platform-express"] ?? "^11.0.20"}",
3149
- "reflect-metadata": "${config.versions["reflect-metadata"] ?? "^0.2.2"}",
3150
- "rxjs": "${config.versions["rxjs"] ?? "^7.8.2"}",
3151
- "@${config.name}/lib": "workspace:*"${config.hasDatabase ? `,
3152
- "@${config.name}/database": "workspace:*"` : ""}
3362
+ "ioredis": "${config.versions["ioredis"] ?? "^5.6.0"}"
3153
3363
  },
3154
3364
  "devDependencies": {
3155
- "@nestjs/cli": "${config.versions["@nestjs/cli"] ?? "^11.0.5"}",
3156
- "@nestjs/testing": "${config.versions["@nestjs/testing"] ?? "^11.0.20"}",
3157
- "@types/node": "${config.versions["@types/node"] ?? "^22.15.3"}",
3158
3365
  "typescript": "${config.versions["typescript"] ?? "^5.8.3"}"
3159
3366
  }
3160
3367
  }
3161
3368
  `;
3162
- }
3163
- tsConfig(config) {
3164
- return `{
3165
- "extends": "@${config.name}/config/typescript/node",
3369
+ }
3370
+ function cacheTsConfig(config) {
3371
+ return `{
3372
+ "extends": "@${config.name}/config/typescript/base",
3166
3373
  "compilerOptions": {
3167
3374
  "outDir": "./dist",
3168
- "rootDir": "./src"
3375
+ "rootDir": "./src",
3376
+ "verbatimModuleSyntax": false
3169
3377
  },
3170
- "include": ["src/**/*"],
3171
- "exclude": ["node_modules", "dist", "test"]
3378
+ "include": ["src/**/*"]
3172
3379
  }
3173
3380
  `;
3174
- }
3175
- mainEntry(config) {
3176
- return `import { NestFactory } from '@nestjs/core';
3177
- import { ValidationPipe } from '@nestjs/common';
3178
- import { AppModule } from './app.module.js';
3179
- import { HttpExceptionFilter } from './common/filters/http-exception.filter.js';
3180
- import { LoggingInterceptor } from './common/interceptors/logging.interceptor.js';
3381
+ }
3382
+ function cacheIndex() {
3383
+ return `export { redis } from './client.js';
3384
+ export { CacheService } from './service.js';
3385
+ `;
3386
+ }
3387
+ function cacheClient() {
3388
+ return `import Redis from 'ioredis';
3181
3389
 
3182
- async function bootstrap() {
3183
- const app = await NestFactory.create(AppModule);
3390
+ const redisUrl = process.env.REDIS_URL ?? 'redis://localhost:6379';
3184
3391
 
3185
- app.enableCors({
3186
- origin: process.env.CORS_ORIGIN ?? 'http://localhost:3000',
3187
- credentials: true,
3188
- });
3392
+ const globalForRedis = globalThis as unknown as {
3393
+ redis: Redis | undefined;
3394
+ };
3189
3395
 
3190
- app.setGlobalPrefix('api');
3191
- app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
3192
- app.useGlobalFilters(new HttpExceptionFilter());
3193
- app.useGlobalInterceptors(new LoggingInterceptor());
3396
+ export const redis = globalForRedis.redis ?? new Redis(redisUrl, {
3397
+ maxRetriesPerRequest: 3,
3398
+ retryStrategy(times) {
3399
+ if (times > 3) return null;
3400
+ return Math.min(times * 200, 2000);
3401
+ },
3402
+ });
3194
3403
 
3195
- const port = process.env.PORT ?? 3001;
3196
- await app.listen(port);
3197
- console.log(\`\u{1F680} ${config.pascalCase} API running on http://localhost:\${port}/api\`);
3404
+ if (process.env.NODE_ENV !== 'production') {
3405
+ globalForRedis.redis = redis;
3198
3406
  }
3199
-
3200
- bootstrap();
3201
3407
  `;
3202
- }
3203
- appSetup(_config) {
3204
- return `import { Module } from '@nestjs/common';
3205
- import { AppController } from './app.controller.js';
3206
- import { AppService } from './app.service.js';
3408
+ }
3409
+ function cacheService() {
3410
+ return `import { redis } from './client.js';
3207
3411
 
3208
- @Module({
3209
- imports: [],
3210
- controllers: [AppController],
3211
- providers: [AppService],
3212
- })
3213
- export class AppModule {}
3214
- `;
3215
- }
3216
- appController(_config) {
3217
- return `import { Controller, Get } from '@nestjs/common';
3218
- import { AppService } from './app.service.js';
3412
+ /** Simple cache wrapper around Redis with JSON serialization and TTL support */
3413
+ export class CacheService {
3414
+ private defaultTtl: number;
3219
3415
 
3220
- @Controller()
3221
- export class AppController {
3222
- constructor(private readonly appService: AppService) {}
3416
+ constructor(defaultTtl = 3600) {
3417
+ this.defaultTtl = defaultTtl;
3418
+ }
3223
3419
 
3224
- @Get('health')
3225
- getHealth() {
3226
- return this.appService.getHealth();
3420
+ /** Get a cached value by key */
3421
+ async get<T>(key: string): Promise<T | null> {
3422
+ const value = await redis.get(key);
3423
+ if (!value) return null;
3424
+ try {
3425
+ return JSON.parse(value) as T;
3426
+ } catch {
3427
+ return value as unknown as T;
3428
+ }
3227
3429
  }
3228
3430
 
3229
- @Get()
3230
- getInfo() {
3231
- return this.appService.getInfo();
3431
+ /** Set a cached value with optional TTL (seconds) */
3432
+ async set<T>(key: string, value: T, ttl?: number): Promise<void> {
3433
+ const serialized = JSON.stringify(value);
3434
+ const expiry = ttl ?? this.defaultTtl;
3435
+ await redis.set(key, serialized, 'EX', expiry);
3232
3436
  }
3233
- }
3234
- `;
3437
+
3438
+ /** Delete a cached value */
3439
+ async del(key: string): Promise<void> {
3440
+ await redis.del(key);
3235
3441
  }
3236
- appService(config) {
3237
- return `import { Injectable } from '@nestjs/common';
3238
3442
 
3239
- @Injectable()
3240
- export class AppService {
3241
- getHealth() {
3242
- return {
3243
- status: 'ok',
3244
- timestamp: new Date().toISOString(),
3245
- uptime: process.uptime(),
3246
- };
3443
+ /** Delete all keys matching a pattern */
3444
+ async delPattern(pattern: string): Promise<void> {
3445
+ const keys = await redis.keys(pattern);
3446
+ if (keys.length > 0) {
3447
+ await redis.del(...keys);
3448
+ }
3247
3449
  }
3248
3450
 
3249
- getInfo() {
3250
- return {
3251
- name: '${config.name}',
3252
- version: '1.0.0',
3253
- environment: process.env.NODE_ENV ?? 'development',
3254
- };
3451
+ /** Check if a key exists */
3452
+ async exists(key: string): Promise<boolean> {
3453
+ return (await redis.exists(key)) === 1;
3255
3454
  }
3256
3455
  }
3257
3456
  `;
3258
- }
3259
- exceptionFilter() {
3260
- return `import {
3261
- ExceptionFilter,
3262
- Catch,
3263
- ArgumentsHost,
3264
- HttpException,
3265
- HttpStatus,
3266
- } from '@nestjs/common';
3267
- import type { Response } from 'express';
3268
-
3269
- @Catch()
3270
- export class HttpExceptionFilter implements ExceptionFilter {
3271
- catch(exception: unknown, host: ArgumentsHost) {
3272
- const ctx = host.switchToHttp();
3273
- const response = ctx.getResponse<Response>();
3457
+ }
3274
3458
 
3275
- const status =
3276
- exception instanceof HttpException
3277
- ? exception.getStatus()
3278
- : HttpStatus.INTERNAL_SERVER_ERROR;
3459
+ // src/templates/i18n-templates.ts
3460
+ function i18nRequest() {
3461
+ return `import { getRequestConfig } from 'next-intl/server';
3462
+ import { routing } from './routing';
3279
3463
 
3280
- const message =
3281
- exception instanceof HttpException
3282
- ? exception.message
3283
- : 'Internal server error';
3464
+ export default getRequestConfig(async ({ requestLocale }) => {
3465
+ let locale = await requestLocale;
3284
3466
 
3285
- response.status(status).json({
3286
- success: false,
3287
- error: {
3288
- code: HttpStatus[status] ?? 'UNKNOWN_ERROR',
3289
- message,
3290
- },
3291
- timestamp: new Date().toISOString(),
3292
- });
3467
+ if (!locale || !routing.locales.includes(locale as any)) {
3468
+ locale = routing.defaultLocale;
3293
3469
  }
3470
+
3471
+ return {
3472
+ locale,
3473
+ messages: (await import(\`../messages/\${locale}.json\`)).default,
3474
+ };
3475
+ });
3476
+ `;
3294
3477
  }
3478
+ function i18nRouting() {
3479
+ return `import { defineRouting } from 'next-intl/routing';
3480
+
3481
+ export const routing = defineRouting({
3482
+ locales: ['en', 'ar'],
3483
+ defaultLocale: 'en',
3484
+ });
3295
3485
  `;
3296
- }
3297
- loggingInterceptor() {
3298
- return `import {
3299
- Injectable,
3300
- NestInterceptor,
3301
- ExecutionContext,
3302
- CallHandler,
3303
- } from '@nestjs/common';
3304
- import { Observable, tap } from 'rxjs';
3486
+ }
3487
+ function i18nNavigation() {
3488
+ return `import { createNavigation } from 'next-intl/navigation';
3489
+ import { routing } from './routing';
3305
3490
 
3306
- @Injectable()
3307
- export class LoggingInterceptor implements NestInterceptor {
3308
- intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
3309
- const request = context.switchToHttp().getRequest();
3310
- const method = request.method;
3311
- const url = request.url;
3312
- const startTime = Date.now();
3491
+ export const { Link, redirect, usePathname, useRouter, getPathname } =
3492
+ createNavigation(routing);
3493
+ `;
3494
+ }
3495
+ function messagesEn(config) {
3496
+ return JSON.stringify(
3497
+ {
3498
+ common: {
3499
+ appName: config.pascalCase,
3500
+ home: "Home",
3501
+ about: "About",
3502
+ contact: "Contact",
3503
+ getStarted: "Get Started",
3504
+ documentation: "Documentation",
3505
+ language: "Language"
3506
+ },
3507
+ home: {
3508
+ title: config.pascalCase,
3509
+ description: `Production-ready monorepo with Next.js 15 and Turborepo.`,
3510
+ frontend: "Frontend",
3511
+ frontendDesc: "Next.js 15 with App Router",
3512
+ backend: "Backend",
3513
+ backendDesc: "REST API server"
3514
+ },
3515
+ notFound: {
3516
+ title: "Page not found",
3517
+ goHome: "Go Home"
3518
+ }
3519
+ },
3520
+ null,
3521
+ 2
3522
+ ) + "\n";
3523
+ }
3524
+ function messagesAr(config) {
3525
+ return JSON.stringify(
3526
+ {
3527
+ common: {
3528
+ appName: config.pascalCase,
3529
+ home: "\u0627\u0644\u0631\u0626\u064A\u0633\u064A\u0629",
3530
+ about: "\u062D\u0648\u0644",
3531
+ contact: "\u0627\u062A\u0635\u0644 \u0628\u0646\u0627",
3532
+ getStarted: "\u0627\u0628\u062F\u0623 \u0627\u0644\u0622\u0646",
3533
+ documentation: "\u0627\u0644\u062A\u0648\u062B\u064A\u0642",
3534
+ language: "\u0627\u0644\u0644\u063A\u0629"
3535
+ },
3536
+ home: {
3537
+ title: config.pascalCase,
3538
+ description: "\u0645\u0633\u062A\u0648\u062F\u0639 \u0625\u0646\u062A\u0627\u062C\u064A \u0645\u062A\u0643\u0627\u0645\u0644 \u0645\u0639 Next.js 15 \u0648 Turborepo.",
3539
+ frontend: "\u0627\u0644\u0648\u0627\u062C\u0647\u0629 \u0627\u0644\u0623\u0645\u0627\u0645\u064A\u0629",
3540
+ frontendDesc: "Next.js 15 \u0645\u0639 App Router",
3541
+ backend: "\u0627\u0644\u062E\u0627\u062F\u0645",
3542
+ backendDesc: "\u062E\u0627\u062F\u0645 REST API"
3543
+ },
3544
+ notFound: {
3545
+ title: "\u0627\u0644\u0635\u0641\u062D\u0629 \u063A\u064A\u0631 \u0645\u0648\u062C\u0648\u062F\u0629",
3546
+ goHome: "\u0627\u0644\u0639\u0648\u062F\u0629 \u0644\u0644\u0631\u0626\u064A\u0633\u064A\u0629"
3547
+ }
3548
+ },
3549
+ null,
3550
+ 2
3551
+ ) + "\n";
3552
+ }
3553
+ function i18nLayout(config) {
3554
+ return `import type { Metadata } from 'next';
3555
+ import { NextIntlClientProvider } from 'next-intl';
3556
+ import { getMessages } from 'next-intl/server';
3557
+ import { notFound } from 'next/navigation';
3558
+ import { routing } from '@/i18n/routing';
3559
+ import '../globals.css';
3313
3560
 
3314
- return next.handle().pipe(
3315
- tap(() => {
3316
- const duration = Date.now() - startTime;
3317
- console.log(\`\${method} \${url} \u2014 \${duration}ms\`);
3318
- }),
3319
- );
3561
+ export const metadata: Metadata = {
3562
+ title: '${config.pascalCase}',
3563
+ description: 'Built with Next.js 15 and Turborepo',
3564
+ };
3565
+
3566
+ export default async function LocaleLayout({
3567
+ children,
3568
+ params,
3569
+ }: {
3570
+ children: React.ReactNode;
3571
+ params: Promise<{ locale: string }>;
3572
+ }) {
3573
+ const { locale } = await params;
3574
+
3575
+ if (!routing.locales.includes(locale as any)) {
3576
+ notFound();
3320
3577
  }
3578
+
3579
+ const messages = await getMessages();
3580
+ const dir = locale === 'ar' ? 'rtl' : 'ltr';
3581
+
3582
+ return (
3583
+ <html lang={locale} dir={dir}>
3584
+ <body>
3585
+ <NextIntlClientProvider messages={messages}>
3586
+ {children}
3587
+ </NextIntlClientProvider>
3588
+ </body>
3589
+ </html>
3590
+ );
3321
3591
  }
3322
3592
  `;
3323
- }
3324
- authGuard() {
3325
- return `import {
3326
- Injectable,
3327
- CanActivate,
3328
- ExecutionContext,
3329
- UnauthorizedException,
3330
- } from '@nestjs/common';
3593
+ }
3594
+ function i18nPage(config) {
3595
+ return `import { useTranslations } from 'next-intl';
3596
+ import { Button, Card } from '@${config.name}/ui';
3331
3597
 
3332
- @Injectable()
3333
- export class AuthGuard implements CanActivate {
3334
- canActivate(context: ExecutionContext): boolean {
3335
- const request = context.switchToHttp().getRequest();
3336
- const authHeader = request.headers.authorization;
3598
+ export default function HomePage() {
3599
+ const t = useTranslations('home');
3600
+ const tc = useTranslations('common');
3337
3601
 
3338
- if (!authHeader?.startsWith('Bearer ')) {
3339
- throw new UnauthorizedException('Missing or invalid authorization header');
3340
- }
3602
+ return (
3603
+ <main style={{
3604
+ minHeight: '100vh',
3605
+ display: 'flex',
3606
+ flexDirection: 'column',
3607
+ alignItems: 'center',
3608
+ justifyContent: 'center',
3609
+ padding: '2rem',
3610
+ }}>
3611
+ <div style={{ maxWidth: '42rem', textAlign: 'center' }}>
3612
+ <h1 style={{ fontSize: '3rem', fontWeight: 'bold', marginBottom: '1rem' }}>
3613
+ {t('title')}
3614
+ </h1>
3615
+ <p style={{ fontSize: '1.25rem', color: '#64748b', marginBottom: '2rem' }}>
3616
+ {t('description')}
3617
+ </p>
3341
3618
 
3342
- // TODO: Validate the token
3343
- return true;
3344
- }
3619
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '1rem', marginBottom: '2rem' }}>
3620
+ <Card title={t('frontend')} description={t('frontendDesc')} />
3621
+ <Card title={t('backend')} description={t('backendDesc')} />
3622
+ </div>
3623
+
3624
+ <div style={{ display: 'flex', gap: '1rem', justifyContent: 'center' }}>
3625
+ <Button variant="primary" size="lg">{tc('getStarted')}</Button>
3626
+ <Button variant="outline" size="lg">{tc('documentation')}</Button>
3627
+ </div>
3628
+ </div>
3629
+ </main>
3630
+ );
3345
3631
  }
3346
3632
  `;
3347
- }
3348
- validationPipe() {
3349
- return `import {
3350
- PipeTransform,
3351
- Injectable,
3352
- ArgumentMetadata,
3353
- BadRequestException,
3354
- } from '@nestjs/common';
3355
- import { ZodSchema, ZodError } from 'zod';
3633
+ }
3634
+ function languageSwitcher() {
3635
+ return `'use client';
3356
3636
 
3357
- @Injectable()
3358
- export class ZodValidationPipe implements PipeTransform {
3359
- constructor(private schema: ZodSchema) {}
3637
+ import { useLocale } from 'next-intl';
3638
+ import { useRouter, usePathname } from '@/i18n/navigation';
3360
3639
 
3361
- transform(value: unknown, _metadata: ArgumentMetadata) {
3362
- try {
3363
- return this.schema.parse(value);
3364
- } catch (error) {
3365
- if (error instanceof ZodError) {
3366
- const details: Record<string, string[]> = {};
3367
- for (const issue of error.issues) {
3368
- const path = issue.path.join('.');
3369
- if (!details[path]) details[path] = [];
3370
- details[path].push(issue.message);
3371
- }
3372
- throw new BadRequestException({
3373
- message: 'Validation failed',
3374
- details,
3375
- });
3376
- }
3377
- throw new BadRequestException('Validation failed');
3378
- }
3640
+ const localeLabels: Record<string, string> = {
3641
+ en: 'English',
3642
+ ar: '\u0627\u0644\u0639\u0631\u0628\u064A\u0629',
3643
+ };
3644
+
3645
+ export function LanguageSwitcher() {
3646
+ const locale = useLocale();
3647
+ const router = useRouter();
3648
+ const pathname = usePathname();
3649
+
3650
+ function handleChange(newLocale: string) {
3651
+ router.replace(pathname, { locale: newLocale });
3379
3652
  }
3653
+
3654
+ return (
3655
+ <select
3656
+ value={locale}
3657
+ onChange={(e) => handleChange(e.target.value)}
3658
+ style={{
3659
+ padding: '6px 12px',
3660
+ borderRadius: '6px',
3661
+ border: '1px solid #d1d5db',
3662
+ background: 'white',
3663
+ cursor: 'pointer',
3664
+ }}
3665
+ >
3666
+ {Object.entries(localeLabels).map(([code, label]) => (
3667
+ <option key={code} value={code}>
3668
+ {label}
3669
+ </option>
3670
+ ))}
3671
+ </select>
3672
+ );
3380
3673
  }
3381
3674
  `;
3382
- }
3675
+ }
3676
+
3677
+ // src/templates/storybook-templates.ts
3678
+ function storybookMain(_config) {
3679
+ return `import type { StorybookConfig } from '@storybook/react-vite';
3680
+
3681
+ const config: StorybookConfig = {
3682
+ stories: ['../src/**/*.stories.@(ts|tsx)'],
3683
+ addons: ['@storybook/addon-essentials'],
3684
+ framework: {
3685
+ name: '@storybook/react-vite',
3686
+ options: {},
3687
+ },
3383
3688
  };
3384
3689
 
3385
- // src/templates/strategies/express-templates.ts
3386
- var ExpressTemplateStrategy = class {
3690
+ export default config;
3691
+ `;
3692
+ }
3693
+ function storybookPreview(_config) {
3694
+ return `import type { Preview } from '@storybook/react';
3695
+
3696
+ const preview: Preview = {
3697
+ parameters: {
3698
+ controls: {
3699
+ matchers: {
3700
+ color: /(background|color)$/i,
3701
+ date: /Date$/i,
3702
+ },
3703
+ },
3704
+ },
3705
+ };
3706
+
3707
+ export default preview;
3708
+ `;
3709
+ }
3710
+ function buttonStories(_config) {
3711
+ return `import type { Meta, StoryObj } from '@storybook/react';
3712
+ import { Button } from './button';
3713
+
3714
+ const meta: Meta<typeof Button> = {
3715
+ title: 'Components/Button',
3716
+ component: Button,
3717
+ tags: ['autodocs'],
3718
+ argTypes: {
3719
+ variant: { control: 'select', options: ['primary', 'secondary', 'outline', 'ghost'] },
3720
+ size: { control: 'select', options: ['sm', 'md', 'lg'] },
3721
+ },
3722
+ };
3723
+
3724
+ export default meta;
3725
+ type Story = StoryObj<typeof Button>;
3726
+
3727
+ export const Primary: Story = {
3728
+ args: { variant: 'primary', children: 'Primary Button' },
3729
+ };
3730
+
3731
+ export const Secondary: Story = {
3732
+ args: { variant: 'secondary', children: 'Secondary Button' },
3733
+ };
3734
+
3735
+ export const Outline: Story = {
3736
+ args: { variant: 'outline', children: 'Outline Button' },
3737
+ };
3738
+
3739
+ export const Ghost: Story = {
3740
+ args: { variant: 'ghost', children: 'Ghost Button' },
3741
+ };
3742
+
3743
+ export const Small: Story = {
3744
+ args: { variant: 'primary', size: 'sm', children: 'Small' },
3745
+ };
3746
+
3747
+ export const Large: Story = {
3748
+ args: { variant: 'primary', size: 'lg', children: 'Large' },
3749
+ };
3750
+ `;
3751
+ }
3752
+ function cardStories(_config) {
3753
+ return `import type { Meta, StoryObj } from '@storybook/react';
3754
+ import { Card } from './card';
3755
+
3756
+ const meta: Meta<typeof Card> = {
3757
+ title: 'Components/Card',
3758
+ component: Card,
3759
+ tags: ['autodocs'],
3760
+ };
3761
+
3762
+ export default meta;
3763
+ type Story = StoryObj<typeof Card>;
3764
+
3765
+ export const Default: Story = {
3766
+ args: {
3767
+ title: 'Card Title',
3768
+ description: 'This is a card description with some example text.',
3769
+ children: 'Card content goes here.',
3770
+ },
3771
+ };
3772
+
3773
+ export const TitleOnly: Story = {
3774
+ args: {
3775
+ title: 'Just a Title',
3776
+ },
3777
+ };
3778
+
3779
+ export const WithContent: Story = {
3780
+ args: {
3781
+ title: 'Interactive Card',
3782
+ description: 'A card with custom content.',
3783
+ children: 'Custom child content rendered inside the card.',
3784
+ },
3785
+ };
3786
+ `;
3787
+ }
3788
+ function inputStories(_config) {
3789
+ return `import type { Meta, StoryObj } from '@storybook/react';
3790
+ import { Input } from './input';
3791
+
3792
+ const meta: Meta<typeof Input> = {
3793
+ title: 'Components/Input',
3794
+ component: Input,
3795
+ tags: ['autodocs'],
3796
+ };
3797
+
3798
+ export default meta;
3799
+ type Story = StoryObj<typeof Input>;
3800
+
3801
+ export const Default: Story = {
3802
+ args: {
3803
+ label: 'Email',
3804
+ placeholder: 'Enter your email',
3805
+ },
3806
+ };
3807
+
3808
+ export const WithError: Story = {
3809
+ args: {
3810
+ label: 'Email',
3811
+ placeholder: 'Enter your email',
3812
+ error: 'Invalid email address',
3813
+ },
3814
+ };
3815
+
3816
+ export const NoLabel: Story = {
3817
+ args: {
3818
+ placeholder: 'Search...',
3819
+ },
3820
+ };
3821
+ `;
3822
+ }
3823
+
3824
+ // src/templates/strategies/nestjs-templates.ts
3825
+ var NestjsTemplateStrategy = class {
3387
3826
  packageJson(config) {
3388
3827
  return `{
3389
3828
  "name": "@${config.name}/api",
3390
3829
  "version": "0.0.0",
3391
3830
  "private": true,
3392
- "type": "module",
3393
3831
  "scripts": {
3394
- "build": "tsc",
3395
- "dev": "tsx watch src/main.ts",
3396
- "start": "node dist/main.js",
3832
+ "build": "nest build",
3833
+ "dev": "nest start --watch",
3834
+ "start": "nest start",
3835
+ "start:prod": "node dist/main",
3397
3836
  "lint": "eslint src --ext .ts",
3398
3837
  "test": "${config.testing === "vitest" ? "vitest run" : "jest"}"
3399
3838
  },
3400
3839
  "dependencies": {
3401
- "express": "${config.versions["express"] ?? "^5.1.0"}",
3402
- "cors": "${config.versions["cors"] ?? "^2.8.5"}",
3840
+ "@nestjs/common": "${config.versions["@nestjs/common"] ?? "^11.0.20"}",
3841
+ "@nestjs/core": "${config.versions["@nestjs/core"] ?? "^11.0.20"}",
3842
+ "@nestjs/platform-express": "${config.versions["@nestjs/platform-express"] ?? "^11.0.20"}",
3843
+ "reflect-metadata": "${config.versions["reflect-metadata"] ?? "^0.2.2"}",
3844
+ "rxjs": "${config.versions["rxjs"] ?? "^7.8.2"}",
3403
3845
  "@${config.name}/lib": "workspace:*"${config.hasDatabase ? `,
3404
3846
  "@${config.name}/database": "workspace:*"` : ""}
3405
3847
  },
3406
3848
  "devDependencies": {
3407
- "@types/express": "${config.versions["@types/express"] ?? "^5.0.2"}",
3408
- "@types/cors": "${config.versions["@types/cors"] ?? "^2.8.17"}",
3849
+ "@nestjs/cli": "${config.versions["@nestjs/cli"] ?? "^11.0.5"}",
3850
+ "@nestjs/testing": "${config.versions["@nestjs/testing"] ?? "^11.0.20"}",
3409
3851
  "@types/node": "${config.versions["@types/node"] ?? "^22.15.3"}",
3410
- "tsx": "^4.19.4",
3411
3852
  "typescript": "${config.versions["typescript"] ?? "^5.8.3"}"
3412
3853
  }
3413
3854
  }
@@ -3415,13 +3856,10 @@ var ExpressTemplateStrategy = class {
3415
3856
  }
3416
3857
  tsConfig(config) {
3417
3858
  return `{
3418
- "extends": "@${config.name}/config/typescript/base",
3859
+ "extends": "@${config.name}/config/typescript/node",
3419
3860
  "compilerOptions": {
3420
3861
  "outDir": "./dist",
3421
- "rootDir": "./src",
3422
- "module": "ESNext",
3423
- "moduleResolution": "bundler",
3424
- "verbatimModuleSyntax": false
3862
+ "rootDir": "./src"
3425
3863
  },
3426
3864
  "include": ["src/**/*"],
3427
3865
  "exclude": ["node_modules", "dist", "test"]
@@ -3429,54 +3867,310 @@ var ExpressTemplateStrategy = class {
3429
3867
  `;
3430
3868
  }
3431
3869
  mainEntry(config) {
3432
- return `import { createApp } from './app.js';
3870
+ return `import { NestFactory } from '@nestjs/core';
3871
+ import { ValidationPipe } from '@nestjs/common';
3872
+ import { AppModule } from './app.module.js';
3873
+ import { HttpExceptionFilter } from './common/filters/http-exception.filter.js';
3874
+ import { LoggingInterceptor } from './common/interceptors/logging.interceptor.js';
3433
3875
 
3434
- const port = process.env.PORT ?? 3001;
3876
+ async function bootstrap() {
3877
+ const app = await NestFactory.create(AppModule);
3435
3878
 
3436
- const app = createApp();
3879
+ app.enableCors({
3880
+ origin: process.env.CORS_ORIGIN ?? 'http://localhost:3000',
3881
+ credentials: true,
3882
+ });
3437
3883
 
3438
- app.listen(port, () => {
3884
+ app.setGlobalPrefix('api');
3885
+ app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
3886
+ app.useGlobalFilters(new HttpExceptionFilter());
3887
+ app.useGlobalInterceptors(new LoggingInterceptor());
3888
+
3889
+ const port = process.env.PORT ?? 3001;
3890
+ await app.listen(port);
3439
3891
  console.log(\`\u{1F680} ${config.pascalCase} API running on http://localhost:\${port}/api\`);
3440
- });
3892
+ }
3893
+
3894
+ bootstrap();
3441
3895
  `;
3442
3896
  }
3443
3897
  appSetup(_config) {
3444
- return `import express from 'express';
3445
- import cors from 'cors';
3446
- import { healthRouter } from './routes/health.js';
3447
- import { errorHandler } from './common/filters/error-handler.js';
3448
- import { requestLogger } from './common/interceptors/request-logger.js';
3449
-
3450
- export function createApp() {
3451
- const app = express();
3898
+ return `import { Module } from '@nestjs/common';
3899
+ import { AppController } from './app.controller.js';
3900
+ import { AppService } from './app.service.js';
3452
3901
 
3453
- // Middleware
3454
- app.use(cors({
3455
- origin: process.env.CORS_ORIGIN ?? 'http://localhost:3000',
3456
- credentials: true,
3457
- }));
3458
- app.use(express.json());
3459
- app.use(express.urlencoded({ extended: true }));
3460
- app.use(requestLogger);
3902
+ @Module({
3903
+ imports: [],
3904
+ controllers: [AppController],
3905
+ providers: [AppService],
3906
+ })
3907
+ export class AppModule {}
3908
+ `;
3909
+ }
3910
+ appController(_config) {
3911
+ return `import { Controller, Get } from '@nestjs/common';
3912
+ import { AppService } from './app.service.js';
3461
3913
 
3462
- // Routes
3463
- app.use('/api', healthRouter);
3914
+ @Controller()
3915
+ export class AppController {
3916
+ constructor(private readonly appService: AppService) {}
3464
3917
 
3465
- // Error handling (must be last)
3466
- app.use(errorHandler);
3918
+ @Get('health')
3919
+ getHealth() {
3920
+ return this.appService.getHealth();
3921
+ }
3467
3922
 
3468
- return app;
3923
+ @Get()
3924
+ getInfo() {
3925
+ return this.appService.getInfo();
3926
+ }
3469
3927
  }
3470
3928
  `;
3471
3929
  }
3472
- appController(config) {
3473
- return `import { Router } from 'express';
3474
- import { AppService } from '../services/app.service.js';
3475
-
3476
- export const healthRouter = Router();
3477
- const appService = new AppService('${config.name}');
3930
+ appService(config) {
3931
+ return `import { Injectable } from '@nestjs/common';
3478
3932
 
3479
- healthRouter.get('/health', (_req, res) => {
3933
+ @Injectable()
3934
+ export class AppService {
3935
+ getHealth() {
3936
+ return {
3937
+ status: 'ok',
3938
+ timestamp: new Date().toISOString(),
3939
+ uptime: process.uptime(),
3940
+ };
3941
+ }
3942
+
3943
+ getInfo() {
3944
+ return {
3945
+ name: '${config.name}',
3946
+ version: '1.0.0',
3947
+ environment: process.env.NODE_ENV ?? 'development',
3948
+ };
3949
+ }
3950
+ }
3951
+ `;
3952
+ }
3953
+ exceptionFilter() {
3954
+ return `import {
3955
+ ExceptionFilter,
3956
+ Catch,
3957
+ ArgumentsHost,
3958
+ HttpException,
3959
+ HttpStatus,
3960
+ } from '@nestjs/common';
3961
+ import type { Response } from 'express';
3962
+
3963
+ @Catch()
3964
+ export class HttpExceptionFilter implements ExceptionFilter {
3965
+ catch(exception: unknown, host: ArgumentsHost) {
3966
+ const ctx = host.switchToHttp();
3967
+ const response = ctx.getResponse<Response>();
3968
+
3969
+ const status =
3970
+ exception instanceof HttpException
3971
+ ? exception.getStatus()
3972
+ : HttpStatus.INTERNAL_SERVER_ERROR;
3973
+
3974
+ const message =
3975
+ exception instanceof HttpException
3976
+ ? exception.message
3977
+ : 'Internal server error';
3978
+
3979
+ response.status(status).json({
3980
+ success: false,
3981
+ error: {
3982
+ code: HttpStatus[status] ?? 'UNKNOWN_ERROR',
3983
+ message,
3984
+ },
3985
+ timestamp: new Date().toISOString(),
3986
+ });
3987
+ }
3988
+ }
3989
+ `;
3990
+ }
3991
+ loggingInterceptor() {
3992
+ return `import {
3993
+ Injectable,
3994
+ NestInterceptor,
3995
+ ExecutionContext,
3996
+ CallHandler,
3997
+ } from '@nestjs/common';
3998
+ import { Observable, tap } from 'rxjs';
3999
+
4000
+ @Injectable()
4001
+ export class LoggingInterceptor implements NestInterceptor {
4002
+ intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
4003
+ const request = context.switchToHttp().getRequest();
4004
+ const method = request.method;
4005
+ const url = request.url;
4006
+ const startTime = Date.now();
4007
+
4008
+ return next.handle().pipe(
4009
+ tap(() => {
4010
+ const duration = Date.now() - startTime;
4011
+ console.log(\`\${method} \${url} \u2014 \${duration}ms\`);
4012
+ }),
4013
+ );
4014
+ }
4015
+ }
4016
+ `;
4017
+ }
4018
+ authGuard() {
4019
+ return `import {
4020
+ Injectable,
4021
+ CanActivate,
4022
+ ExecutionContext,
4023
+ UnauthorizedException,
4024
+ } from '@nestjs/common';
4025
+
4026
+ @Injectable()
4027
+ export class AuthGuard implements CanActivate {
4028
+ canActivate(context: ExecutionContext): boolean {
4029
+ const request = context.switchToHttp().getRequest();
4030
+ const authHeader = request.headers.authorization;
4031
+
4032
+ if (!authHeader?.startsWith('Bearer ')) {
4033
+ throw new UnauthorizedException('Missing or invalid authorization header');
4034
+ }
4035
+
4036
+ // TODO: Validate the token
4037
+ return true;
4038
+ }
4039
+ }
4040
+ `;
4041
+ }
4042
+ validationPipe() {
4043
+ return `import {
4044
+ PipeTransform,
4045
+ Injectable,
4046
+ ArgumentMetadata,
4047
+ BadRequestException,
4048
+ } from '@nestjs/common';
4049
+ import { ZodSchema, ZodError } from 'zod';
4050
+
4051
+ @Injectable()
4052
+ export class ZodValidationPipe implements PipeTransform {
4053
+ constructor(private schema: ZodSchema) {}
4054
+
4055
+ transform(value: unknown, _metadata: ArgumentMetadata) {
4056
+ try {
4057
+ return this.schema.parse(value);
4058
+ } catch (error) {
4059
+ if (error instanceof ZodError) {
4060
+ const details: Record<string, string[]> = {};
4061
+ for (const issue of error.issues) {
4062
+ const path = issue.path.join('.');
4063
+ if (!details[path]) details[path] = [];
4064
+ details[path].push(issue.message);
4065
+ }
4066
+ throw new BadRequestException({
4067
+ message: 'Validation failed',
4068
+ details,
4069
+ });
4070
+ }
4071
+ throw new BadRequestException('Validation failed');
4072
+ }
4073
+ }
4074
+ }
4075
+ `;
4076
+ }
4077
+ };
4078
+
4079
+ // src/templates/strategies/express-templates.ts
4080
+ var ExpressTemplateStrategy = class {
4081
+ packageJson(config) {
4082
+ return `{
4083
+ "name": "@${config.name}/api",
4084
+ "version": "0.0.0",
4085
+ "private": true,
4086
+ "type": "module",
4087
+ "scripts": {
4088
+ "build": "tsc",
4089
+ "dev": "tsx watch src/main.ts",
4090
+ "start": "node dist/main.js",
4091
+ "lint": "eslint src --ext .ts",
4092
+ "test": "${config.testing === "vitest" ? "vitest run" : "jest"}"
4093
+ },
4094
+ "dependencies": {
4095
+ "express": "${config.versions["express"] ?? "^5.1.0"}",
4096
+ "cors": "${config.versions["cors"] ?? "^2.8.5"}",
4097
+ "@${config.name}/lib": "workspace:*"${config.hasDatabase ? `,
4098
+ "@${config.name}/database": "workspace:*"` : ""}
4099
+ },
4100
+ "devDependencies": {
4101
+ "@types/express": "${config.versions["@types/express"] ?? "^5.0.2"}",
4102
+ "@types/cors": "${config.versions["@types/cors"] ?? "^2.8.17"}",
4103
+ "@types/node": "${config.versions["@types/node"] ?? "^22.15.3"}",
4104
+ "tsx": "^4.19.4",
4105
+ "typescript": "${config.versions["typescript"] ?? "^5.8.3"}"
4106
+ }
4107
+ }
4108
+ `;
4109
+ }
4110
+ tsConfig(config) {
4111
+ return `{
4112
+ "extends": "@${config.name}/config/typescript/base",
4113
+ "compilerOptions": {
4114
+ "outDir": "./dist",
4115
+ "rootDir": "./src",
4116
+ "module": "ESNext",
4117
+ "moduleResolution": "bundler",
4118
+ "verbatimModuleSyntax": false
4119
+ },
4120
+ "include": ["src/**/*"],
4121
+ "exclude": ["node_modules", "dist", "test"]
4122
+ }
4123
+ `;
4124
+ }
4125
+ mainEntry(config) {
4126
+ return `import { createApp } from './app.js';
4127
+
4128
+ const port = process.env.PORT ?? 3001;
4129
+
4130
+ const app = createApp();
4131
+
4132
+ app.listen(port, () => {
4133
+ console.log(\`\u{1F680} ${config.pascalCase} API running on http://localhost:\${port}/api\`);
4134
+ });
4135
+ `;
4136
+ }
4137
+ appSetup(_config) {
4138
+ return `import express from 'express';
4139
+ import cors from 'cors';
4140
+ import { healthRouter } from './routes/health.js';
4141
+ import { errorHandler } from './common/filters/error-handler.js';
4142
+ import { requestLogger } from './common/interceptors/request-logger.js';
4143
+
4144
+ export function createApp() {
4145
+ const app = express();
4146
+
4147
+ // Middleware
4148
+ app.use(cors({
4149
+ origin: process.env.CORS_ORIGIN ?? 'http://localhost:3000',
4150
+ credentials: true,
4151
+ }));
4152
+ app.use(express.json());
4153
+ app.use(express.urlencoded({ extended: true }));
4154
+ app.use(requestLogger);
4155
+
4156
+ // Routes
4157
+ app.use('/api', healthRouter);
4158
+
4159
+ // Error handling (must be last)
4160
+ app.use(errorHandler);
4161
+
4162
+ return app;
4163
+ }
4164
+ `;
4165
+ }
4166
+ appController(config) {
4167
+ return `import { Router } from 'express';
4168
+ import { AppService } from '../services/app.service.js';
4169
+
4170
+ export const healthRouter = Router();
4171
+ const appService = new AppService('${config.name}');
4172
+
4173
+ healthRouter.get('/health', (_req, res) => {
3480
4174
  res.json(appService.getHealth());
3481
4175
  });
3482
4176
 
@@ -4277,175 +4971,2164 @@ type Theme = 'light' | 'dark' | 'system';
4277
4971
  /** Persisted theme atom */
4278
4972
  export const themeAtom = atomWithStorage<Theme>('theme', 'system');
4279
4973
 
4280
- /** Derived atom that resolves 'system' to actual theme */
4281
- export const resolvedThemeAtom = atom((get) => {
4282
- const theme = get(themeAtom);
4283
- if (theme !== 'system') return theme;
4974
+ /** Derived atom that resolves 'system' to actual theme */
4975
+ export const resolvedThemeAtom = atom((get) => {
4976
+ const theme = get(themeAtom);
4977
+ if (theme !== 'system') return theme;
4978
+
4979
+ if (typeof window === 'undefined') return 'light';
4980
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
4981
+ });
4982
+
4983
+ /** Convenience hook for theme management */
4984
+ export function useTheme() {
4985
+ const [theme, setTheme] = useAtom(themeAtom);
4986
+ return { theme, setTheme };
4987
+ }
4988
+ `;
4989
+ }
4990
+ /** Jotai works without a provider (uses default store) */
4991
+ providerWrapper() {
4992
+ return "";
4993
+ }
4994
+ };
4995
+
4996
+ // src/templates/strategies/redux-templates.ts
4997
+ var ReduxTemplateStrategy = class {
4998
+ storeSetup() {
4999
+ return `import { configureStore } from '@reduxjs/toolkit';
5000
+ import { useDispatch, useSelector } from 'react-redux';
5001
+ import type { TypedUseSelectorHook } from 'react-redux';
5002
+ import { themeReducer } from './theme-slice.js';
5003
+
5004
+ export const store = configureStore({
5005
+ reducer: {
5006
+ theme: themeReducer,
5007
+ },
5008
+ });
5009
+
5010
+ export type RootState = ReturnType<typeof store.getState>;
5011
+ export type AppDispatch = typeof store.dispatch;
5012
+
5013
+ /** Typed dispatch hook */
5014
+ export const useAppDispatch: () => AppDispatch = useDispatch;
5015
+
5016
+ /** Typed selector hook */
5017
+ export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
5018
+ `;
5019
+ }
5020
+ exampleStore() {
5021
+ return `import { createSlice } from '@reduxjs/toolkit';
5022
+ import type { PayloadAction } from '@reduxjs/toolkit';
5023
+
5024
+ type Theme = 'light' | 'dark' | 'system';
5025
+
5026
+ interface ThemeState {
5027
+ theme: Theme;
5028
+ }
5029
+
5030
+ const initialState: ThemeState = {
5031
+ theme: 'system',
5032
+ };
5033
+
5034
+ const themeSlice = createSlice({
5035
+ name: 'theme',
5036
+ initialState,
5037
+ reducers: {
5038
+ setTheme(state, action: PayloadAction<Theme>) {
5039
+ state.theme = action.payload;
5040
+ },
5041
+ toggleTheme(state) {
5042
+ state.theme = state.theme === 'dark' ? 'light' : 'dark';
5043
+ },
5044
+ },
5045
+ });
5046
+
5047
+ export const { setTheme, toggleTheme } = themeSlice.actions;
5048
+ export const themeReducer = themeSlice.reducer;
5049
+ `;
5050
+ }
5051
+ providerWrapper() {
5052
+ return `'use client';
5053
+
5054
+ import { Provider } from 'react-redux';
5055
+ import { store } from './store';
5056
+
5057
+ export function StoreProvider({ children }: { children: React.ReactNode }) {
5058
+ return <Provider store={store}>{children}</Provider>;
5059
+ }
5060
+ `;
5061
+ }
5062
+ };
5063
+
5064
+ // src/templates/strategies/tanstack-query-templates.ts
5065
+ var TanstackQueryTemplateStrategy = class {
5066
+ storeSetup() {
5067
+ return `/**
5068
+ * TanStack Query setup and utilities.
5069
+ */
5070
+
5071
+ export { queryClient } from './query-client.js';
5072
+ export { QueryProvider } from './provider.js';
5073
+ `;
5074
+ }
5075
+ exampleStore(_config) {
5076
+ return `import { QueryClient } from '@tanstack/react-query';
5077
+
5078
+ export const queryClient = new QueryClient({
5079
+ defaultOptions: {
5080
+ queries: {
5081
+ staleTime: 60 * 1000, // 1 minute
5082
+ retry: 1,
5083
+ refetchOnWindowFocus: false,
5084
+ },
5085
+ mutations: {
5086
+ retry: 0,
5087
+ },
5088
+ },
5089
+ });
5090
+
5091
+ /**
5092
+ * Example query hook \u2014 fetches health status from the API.
5093
+ */
5094
+ export function useHealthQuery() {
5095
+ const apiUrl = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3001';
5096
+
5097
+ return {
5098
+ queryKey: ['health'],
5099
+ queryFn: async () => {
5100
+ const response = await fetch(\`\${apiUrl}/api/health\`);
5101
+ if (!response.ok) throw new Error('API health check failed');
5102
+ return response.json();
5103
+ },
5104
+ };
5105
+ }
5106
+ `;
5107
+ }
5108
+ providerWrapper() {
5109
+ return `'use client';
5110
+
5111
+ import { QueryClientProvider } from '@tanstack/react-query';
5112
+ import { queryClient } from './query-client';
5113
+
5114
+ export function QueryProvider({ children }: { children: React.ReactNode }) {
5115
+ return (
5116
+ <QueryClientProvider client={queryClient}>
5117
+ {children}
5118
+ </QueryClientProvider>
5119
+ );
5120
+ }
5121
+ `;
5122
+ }
5123
+ };
5124
+
5125
+ // src/templates/strategies/state-factory.ts
5126
+ function createStateStrategy(state) {
5127
+ switch (state) {
5128
+ case "zustand":
5129
+ return new ZustandTemplateStrategy();
5130
+ case "jotai":
5131
+ return new JotaiTemplateStrategy();
5132
+ case "redux":
5133
+ return new ReduxTemplateStrategy();
5134
+ case "tanstack-query":
5135
+ return new TanstackQueryTemplateStrategy();
5136
+ case "none":
5137
+ return null;
5138
+ }
5139
+ }
5140
+
5141
+ // src/templates/strategies/docker-full-templates.ts
5142
+ var DockerFullTemplateStrategy = class {
5143
+ webDockerfile(config) {
5144
+ return `# \u2500\u2500 Stage 1: Dependencies \u2500\u2500
5145
+ FROM node:20-alpine AS deps
5146
+ RUN apk add --no-cache libc6-compat
5147
+ WORKDIR /app
5148
+ COPY package.json ${config.packageManager === "pnpm" ? "pnpm-lock.yaml pnpm-workspace.yaml" : "package-lock.json"} ./
5149
+ COPY apps/web/package.json ./apps/web/
5150
+ COPY packages/ui/package.json ./packages/ui/
5151
+ COPY packages/lib/package.json ./packages/lib/
5152
+ COPY packages/config/package.json ./packages/config/
5153
+ ${config.packageManager === "pnpm" ? "RUN corepack enable pnpm && pnpm install --frozen-lockfile" : "RUN npm ci"}
5154
+
5155
+ # \u2500\u2500 Stage 2: Build \u2500\u2500
5156
+ FROM node:20-alpine AS builder
5157
+ WORKDIR /app
5158
+ COPY --from=deps /app/node_modules ./node_modules
5159
+ COPY . .
5160
+ RUN ${config.packageManager === "pnpm" ? "corepack enable pnpm && pnpm" : "npm run"} build --filter=@${config.name}/web
5161
+
5162
+ # \u2500\u2500 Stage 3: Runner \u2500\u2500
5163
+ FROM node:20-alpine AS runner
5164
+ WORKDIR /app
5165
+ ENV NODE_ENV=production
5166
+ RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 nextjs
5167
+
5168
+ COPY --from=builder /app/apps/web/public ./apps/web/public
5169
+ COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
5170
+ COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
5171
+
5172
+ USER nextjs
5173
+ EXPOSE 3000
5174
+ ENV PORT=3000 HOSTNAME="0.0.0.0"
5175
+ CMD ["node", "apps/web/server.js"]
5176
+ `;
5177
+ }
5178
+ apiDockerfile(config) {
5179
+ return `# \u2500\u2500 Stage 1: Dependencies \u2500\u2500
5180
+ FROM node:20-alpine AS deps
5181
+ WORKDIR /app
5182
+ COPY package.json ${config.packageManager === "pnpm" ? "pnpm-lock.yaml pnpm-workspace.yaml" : "package-lock.json"} ./
5183
+ COPY apps/api/package.json ./apps/api/
5184
+ COPY packages/lib/package.json ./packages/lib/
5185
+ COPY packages/config/package.json ./packages/config/
5186
+ ${config.hasDatabase ? `COPY packages/database/package.json ./packages/database/` : ""}
5187
+ ${config.packageManager === "pnpm" ? "RUN corepack enable pnpm && pnpm install --frozen-lockfile" : "RUN npm ci"}
5188
+
5189
+ # \u2500\u2500 Stage 2: Build \u2500\u2500
5190
+ FROM node:20-alpine AS builder
5191
+ WORKDIR /app
5192
+ COPY --from=deps /app/node_modules ./node_modules
5193
+ COPY . .
5194
+ RUN ${config.packageManager === "pnpm" ? "corepack enable pnpm && pnpm" : "npm run"} build --filter=@${config.name}/api
5195
+
5196
+ # \u2500\u2500 Stage 3: Runner \u2500\u2500
5197
+ FROM node:20-alpine AS runner
5198
+ WORKDIR /app
5199
+ ENV NODE_ENV=production
5200
+ RUN addgroup --system --gid 1001 nodejs && adduser --system --uid 1001 appuser
5201
+
5202
+ COPY --from=builder --chown=appuser:nodejs /app/apps/api/dist ./dist
5203
+ COPY --from=builder /app/node_modules ./node_modules
5204
+ ${config.orm === "prisma" ? "COPY --from=builder /app/packages/database/prisma ./prisma" : ""}
5205
+
5206
+ USER appuser
5207
+ EXPOSE 3001
5208
+ ENV PORT=3001
5209
+ CMD ["node", "dist/main.js"]
5210
+ `;
5211
+ }
5212
+ dockerComposeProd(config) {
5213
+ let services = ` web:
5214
+ build:
5215
+ context: .
5216
+ dockerfile: apps/web/Dockerfile
5217
+ ports:
5218
+ - "3000:3000"
5219
+ environment:
5220
+ - NEXT_PUBLIC_API_URL=http://api:3001
5221
+ depends_on:
5222
+ - api
5223
+ restart: unless-stopped
5224
+
5225
+ api:
5226
+ build:
5227
+ context: .
5228
+ dockerfile: apps/api/Dockerfile
5229
+ ports:
5230
+ - "3001:3001"
5231
+ env_file:
5232
+ - .env
5233
+ restart: unless-stopped
5234
+ `;
5235
+ if (config.hasDatabase && config.db !== "sqlite") {
5236
+ services += ` depends_on:
5237
+ - db
5238
+ `;
5239
+ }
5240
+ if (config.hasCache) {
5241
+ services += `
5242
+ redis:
5243
+ image: redis:7-alpine
5244
+ container_name: ${config.name}-redis
5245
+ ports:
5246
+ - "6379:6379"
5247
+ restart: unless-stopped
5248
+ `;
5249
+ }
5250
+ services += `
5251
+ nginx:
5252
+ build:
5253
+ context: ./nginx
5254
+ dockerfile: Dockerfile
5255
+ ports:
5256
+ - "80:80"
5257
+ depends_on:
5258
+ - web
5259
+ - api
5260
+ restart: unless-stopped
5261
+ `;
5262
+ return `services:
5263
+ ${services}`;
5264
+ }
5265
+ dockerignore(_config) {
5266
+ return `node_modules
5267
+ .next
5268
+ dist
5269
+ .git
5270
+ .gitignore
5271
+ *.md
5272
+ .env*
5273
+ !.env.example
5274
+ coverage
5275
+ .turbo
5276
+ `;
5277
+ }
5278
+ extraFiles(_config) {
5279
+ return {
5280
+ "nginx/nginx.conf": `events {
5281
+ worker_connections 1024;
5282
+ }
5283
+
5284
+ http {
5285
+ upstream web {
5286
+ server web:3000;
5287
+ }
5288
+
5289
+ upstream api {
5290
+ server api:3001;
5291
+ }
5292
+
5293
+ server {
5294
+ listen 80;
5295
+ server_name localhost;
5296
+
5297
+ location / {
5298
+ proxy_pass http://web;
5299
+ proxy_set_header Host $host;
5300
+ proxy_set_header X-Real-IP $remote_addr;
5301
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
5302
+ proxy_set_header X-Forwarded-Proto $scheme;
5303
+ }
5304
+
5305
+ location /api/ {
5306
+ proxy_pass http://api;
5307
+ proxy_set_header Host $host;
5308
+ proxy_set_header X-Real-IP $remote_addr;
5309
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
5310
+ proxy_set_header X-Forwarded-Proto $scheme;
5311
+ }
5312
+ }
5313
+ }
5314
+ `,
5315
+ "nginx/Dockerfile": `FROM nginx:alpine
5316
+ COPY nginx.conf /etc/nginx/nginx.conf
5317
+ EXPOSE 80
5318
+ CMD ["nginx", "-g", "daemon off;"]
5319
+ `
5320
+ };
5321
+ }
5322
+ };
5323
+
5324
+ // src/templates/strategies/docker-minimal-templates.ts
5325
+ var DockerMinimalTemplateStrategy = class {
5326
+ webDockerfile(config) {
5327
+ return `FROM node:20-alpine
5328
+ WORKDIR /app
5329
+ COPY . .
5330
+ ${config.packageManager === "pnpm" ? "RUN corepack enable pnpm && pnpm install --frozen-lockfile" : "RUN npm ci"}
5331
+ RUN ${config.packageManager === "pnpm" ? "pnpm" : "npm run"} build --filter=@${config.name}/web
5332
+ EXPOSE 3000
5333
+ CMD ["node", "apps/web/.next/standalone/apps/web/server.js"]
5334
+ `;
5335
+ }
5336
+ apiDockerfile(config) {
5337
+ return `FROM node:20-alpine
5338
+ WORKDIR /app
5339
+ COPY . .
5340
+ ${config.packageManager === "pnpm" ? "RUN corepack enable pnpm && pnpm install --frozen-lockfile" : "RUN npm ci"}
5341
+ RUN ${config.packageManager === "pnpm" ? "pnpm" : "npm run"} build --filter=@${config.name}/api
5342
+ EXPOSE 3001
5343
+ CMD ["node", "apps/api/dist/main.js"]
5344
+ `;
5345
+ }
5346
+ dockerComposeProd(_config) {
5347
+ return `services:
5348
+ web:
5349
+ build:
5350
+ context: .
5351
+ dockerfile: apps/web/Dockerfile
5352
+ ports:
5353
+ - "3000:3000"
5354
+ environment:
5355
+ - NEXT_PUBLIC_API_URL=http://api:3001
5356
+ depends_on:
5357
+ - api
5358
+ restart: unless-stopped
5359
+
5360
+ api:
5361
+ build:
5362
+ context: .
5363
+ dockerfile: apps/api/Dockerfile
5364
+ ports:
5365
+ - "3001:3001"
5366
+ env_file:
5367
+ - .env
5368
+ restart: unless-stopped
5369
+ `;
5370
+ }
5371
+ dockerignore(_config) {
5372
+ return `node_modules
5373
+ .next
5374
+ dist
5375
+ .git
5376
+ *.md
5377
+ .env*
5378
+ !.env.example
5379
+ coverage
5380
+ .turbo
5381
+ `;
5382
+ }
5383
+ extraFiles(_config) {
5384
+ return {};
5385
+ }
5386
+ };
5387
+
5388
+ // src/templates/strategies/docker-factory.ts
5389
+ function createDockerStrategy(docker) {
5390
+ switch (docker) {
5391
+ case "full":
5392
+ return new DockerFullTemplateStrategy();
5393
+ case "minimal":
5394
+ return new DockerMinimalTemplateStrategy();
5395
+ case "none":
5396
+ return null;
5397
+ }
5398
+ }
5399
+
5400
+ // src/templates/strategies/playwright-templates.ts
5401
+ var PlaywrightTemplateStrategy = class {
5402
+ config(_config) {
5403
+ return `import { defineConfig, devices } from '@playwright/test';
5404
+
5405
+ export default defineConfig({
5406
+ testDir: './e2e',
5407
+ fullyParallel: true,
5408
+ forbidOnly: !!process.env.CI,
5409
+ retries: process.env.CI ? 2 : 0,
5410
+ workers: process.env.CI ? 1 : undefined,
5411
+ reporter: process.env.CI ? 'github' : 'html',
5412
+ use: {
5413
+ baseURL: 'http://localhost:3000',
5414
+ trace: 'on-first-retry',
5415
+ screenshot: 'only-on-failure',
5416
+ },
5417
+ projects: [
5418
+ { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
5419
+ { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
5420
+ { name: 'webkit', use: { ...devices['Desktop Safari'] } },
5421
+ ],
5422
+ webServer: {
5423
+ command: 'pnpm dev',
5424
+ url: 'http://localhost:3000',
5425
+ reuseExistingServer: !process.env.CI,
5426
+ timeout: 120_000,
5427
+ },
5428
+ });
5429
+ `;
5430
+ }
5431
+ exampleTest(_config) {
5432
+ return `import { test, expect } from '@playwright/test';
5433
+
5434
+ test.describe('Home Page', () => {
5435
+ test('should display the heading', async ({ page }) => {
5436
+ await page.goto('/');
5437
+ await expect(page.locator('h1')).toBeVisible();
5438
+ });
5439
+
5440
+ test('should have correct title', async ({ page }) => {
5441
+ await page.goto('/');
5442
+ await expect(page).toHaveTitle(/.*$/);
5443
+ });
5444
+
5445
+ test('should navigate without errors', async ({ page }) => {
5446
+ const response = await page.goto('/');
5447
+ expect(response?.status()).toBeLessThan(400);
5448
+ });
5449
+ });
5450
+ `;
5451
+ }
5452
+ ciWorkflow(_config) {
5453
+ return `
5454
+ e2e:
5455
+ name: E2E Tests
5456
+ runs-on: ubuntu-latest
5457
+ needs: ci
5458
+ steps:
5459
+ - name: Checkout
5460
+ uses: actions/checkout@v4
5461
+
5462
+ - name: Setup Node.js
5463
+ uses: actions/setup-node@v4
5464
+ with:
5465
+ node-version: 20
5466
+
5467
+ - name: Install dependencies
5468
+ run: pnpm install --frozen-lockfile
5469
+
5470
+ - name: Install Playwright browsers
5471
+ run: npx playwright install --with-deps chromium
5472
+
5473
+ - name: Build
5474
+ run: pnpm build
5475
+
5476
+ - name: Run E2E tests
5477
+ run: cd apps/web && npx playwright test --project=chromium
5478
+ `;
5479
+ }
5480
+ };
5481
+
5482
+ // src/templates/strategies/cypress-templates.ts
5483
+ var CypressTemplateStrategy = class {
5484
+ config(_config) {
5485
+ return `import { defineConfig } from 'cypress';
5486
+
5487
+ export default defineConfig({
5488
+ e2e: {
5489
+ baseUrl: 'http://localhost:3000',
5490
+ supportFile: false,
5491
+ specPattern: 'cypress/e2e/**/*.cy.{ts,tsx}',
5492
+ viewportWidth: 1280,
5493
+ viewportHeight: 720,
5494
+ video: false,
5495
+ screenshotOnRunFailure: true,
5496
+ },
5497
+ });
5498
+ `;
5499
+ }
5500
+ exampleTest(_config) {
5501
+ return `describe('Home Page', () => {
5502
+ beforeEach(() => {
5503
+ cy.visit('/');
5504
+ });
5505
+
5506
+ it('should display the heading', () => {
5507
+ cy.get('h1').should('be.visible');
5508
+ });
5509
+
5510
+ it('should load without errors', () => {
5511
+ cy.request('/').its('status').should('be.lessThan', 400);
5512
+ });
5513
+
5514
+ it('should have navigation elements', () => {
5515
+ cy.get('button').should('exist');
5516
+ });
5517
+ });
5518
+ `;
5519
+ }
5520
+ ciWorkflow(_config) {
5521
+ return `
5522
+ e2e:
5523
+ name: E2E Tests
5524
+ runs-on: ubuntu-latest
5525
+ needs: ci
5526
+ steps:
5527
+ - name: Checkout
5528
+ uses: actions/checkout@v4
5529
+
5530
+ - name: Setup Node.js
5531
+ uses: actions/setup-node@v4
5532
+ with:
5533
+ node-version: 20
5534
+
5535
+ - name: Install dependencies
5536
+ run: pnpm install --frozen-lockfile
5537
+
5538
+ - name: Build
5539
+ run: pnpm build
5540
+
5541
+ - name: Run Cypress tests
5542
+ uses: cypress-io/github-action@v6
5543
+ with:
5544
+ working-directory: apps/web
5545
+ start: pnpm start
5546
+ wait-on: 'http://localhost:3000'
5547
+ `;
5548
+ }
5549
+ };
5550
+
5551
+ // src/templates/strategies/e2e-factory.ts
5552
+ function createE2eStrategy(e2e) {
5553
+ switch (e2e) {
5554
+ case "playwright":
5555
+ return new PlaywrightTemplateStrategy();
5556
+ case "cypress":
5557
+ return new CypressTemplateStrategy();
5558
+ case "none":
5559
+ return null;
5560
+ }
5561
+ }
5562
+
5563
+ // src/templates/strategies/pino-templates.ts
5564
+ var PinoTemplateStrategy = class {
5565
+ loggerSetup(_config) {
5566
+ return `import pino from 'pino';
5567
+
5568
+ const isProduction = process.env.NODE_ENV === 'production';
5569
+
5570
+ export const logger = pino({
5571
+ level: process.env.LOG_LEVEL ?? (isProduction ? 'info' : 'debug'),
5572
+ ...(isProduction
5573
+ ? {}
5574
+ : {
5575
+ transport: {
5576
+ target: 'pino-pretty',
5577
+ options: {
5578
+ colorize: true,
5579
+ translateTime: 'HH:MM:ss',
5580
+ ignore: 'pid,hostname',
5581
+ },
5582
+ },
5583
+ }),
5584
+ });
5585
+
5586
+ /** Create a child logger with a context label */
5587
+ export function createLogger(context: string) {
5588
+ return logger.child({ context });
5589
+ }
5590
+ `;
5591
+ }
5592
+ index(_config) {
5593
+ return `export { logger, createLogger } from './logger.js';
5594
+ `;
5595
+ }
5596
+ };
5597
+
5598
+ // src/templates/strategies/winston-templates.ts
5599
+ var WinstonTemplateStrategy = class {
5600
+ loggerSetup(_config) {
5601
+ return `import winston from 'winston';
5602
+
5603
+ const isProduction = process.env.NODE_ENV === 'production';
5604
+
5605
+ export const logger = winston.createLogger({
5606
+ level: process.env.LOG_LEVEL ?? (isProduction ? 'info' : 'debug'),
5607
+ format: isProduction
5608
+ ? winston.format.combine(winston.format.timestamp(), winston.format.json())
5609
+ : winston.format.combine(
5610
+ winston.format.colorize(),
5611
+ winston.format.timestamp({ format: 'HH:mm:ss' }),
5612
+ winston.format.printf(({ timestamp, level, message, context, ...meta }) => {
5613
+ const ctx = context ? \` [\${context}]\` : '';
5614
+ const metaStr = Object.keys(meta).length ? \` \${JSON.stringify(meta)}\` : '';
5615
+ return \`\${timestamp} \${level}\${ctx}: \${message}\${metaStr}\`;
5616
+ }),
5617
+ ),
5618
+ transports: [
5619
+ new winston.transports.Console(),
5620
+ ],
5621
+ });
5622
+
5623
+ /** Create a child logger with a context label */
5624
+ export function createLogger(context: string) {
5625
+ return logger.child({ context });
5626
+ }
5627
+ `;
5628
+ }
5629
+ index(_config) {
5630
+ return `export { logger, createLogger } from './logger.js';
5631
+ `;
5632
+ }
5633
+ };
5634
+
5635
+ // src/templates/strategies/logging-factory.ts
5636
+ function createLoggingStrategy(logging) {
5637
+ switch (logging) {
5638
+ case "pino":
5639
+ return new PinoTemplateStrategy();
5640
+ case "winston":
5641
+ return new WinstonTemplateStrategy();
5642
+ case "default":
5643
+ return null;
5644
+ }
5645
+ }
5646
+
5647
+ // src/templates/strategies/resend-templates.ts
5648
+ var ResendTemplateStrategy = class {
5649
+ packageJson(config) {
5650
+ return `{
5651
+ "name": "@${config.name}/email",
5652
+ "version": "1.0.0",
5653
+ "private": true,
5654
+ "type": "module",
5655
+ "main": "./src/index.ts",
5656
+ "types": "./src/index.ts",
5657
+ "exports": {
5658
+ ".": "./src/index.ts"
5659
+ },
5660
+ "dependencies": {
5661
+ "resend": "${config.versions["resend"] ?? "^4.1.0"}",
5662
+ "@react-email/components": "${config.versions["@react-email/components"] ?? "^0.0.36"}",
5663
+ "react": "${config.versions["react"] ?? "^19.1.0"}"
5664
+ },
5665
+ "devDependencies": {
5666
+ "typescript": "${config.versions["typescript"] ?? "^5.8.3"}"
5667
+ }
5668
+ }
5669
+ `;
5670
+ }
5671
+ index(_config) {
5672
+ return `export { sendEmail } from './send.js';
5673
+ export { resend } from './client.js';
5674
+ `;
5675
+ }
5676
+ client(_config) {
5677
+ return `import { Resend } from 'resend';
5678
+
5679
+ export const resend = new Resend(process.env.RESEND_API_KEY);
5680
+ `;
5681
+ }
5682
+ sendFunction(_config) {
5683
+ return `import { resend } from './client.js';
5684
+
5685
+ interface SendEmailOptions {
5686
+ to: string | string[];
5687
+ subject: string;
5688
+ react: React.ReactElement;
5689
+ }
5690
+
5691
+ export async function sendEmail({ to, subject, react }: SendEmailOptions) {
5692
+ const { data, error } = await resend.emails.send({
5693
+ from: process.env.EMAIL_FROM ?? 'noreply@example.com',
5694
+ to: Array.isArray(to) ? to : [to],
5695
+ subject,
5696
+ react,
5697
+ });
5698
+
5699
+ if (error) {
5700
+ throw new Error(\`Failed to send email: \${error.message}\`);
5701
+ }
5702
+
5703
+ return data;
5704
+ }
5705
+ `;
5706
+ }
5707
+ welcomeTemplate(config) {
5708
+ return `import { Html, Head, Body, Container, Heading, Text, Button } from '@react-email/components';
5709
+
5710
+ interface WelcomeEmailProps {
5711
+ name: string;
5712
+ }
5713
+
5714
+ export function WelcomeEmail({ name }: WelcomeEmailProps) {
5715
+ return (
5716
+ <Html>
5717
+ <Head />
5718
+ <Body style={{ fontFamily: 'system-ui, sans-serif', background: '#f9fafb' }}>
5719
+ <Container style={{ maxWidth: '600px', margin: '0 auto', padding: '40px 20px' }}>
5720
+ <Heading style={{ fontSize: '24px', color: '#111827' }}>
5721
+ Welcome to ${config.pascalCase}!
5722
+ </Heading>
5723
+ <Text style={{ fontSize: '16px', color: '#4b5563' }}>
5724
+ Hi {name}, thank you for signing up. We're excited to have you on board.
5725
+ </Text>
5726
+ <Button
5727
+ href={process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000'}
5728
+ style={{ background: '#2563eb', color: 'white', padding: '12px 24px', borderRadius: '8px', textDecoration: 'none' }}
5729
+ >
5730
+ Get Started
5731
+ </Button>
5732
+ </Container>
5733
+ </Body>
5734
+ </Html>
5735
+ );
5736
+ }
5737
+ `;
5738
+ }
5739
+ resetPasswordTemplate(config) {
5740
+ return `import { Html, Head, Body, Container, Heading, Text, Button } from '@react-email/components';
5741
+
5742
+ interface ResetPasswordEmailProps {
5743
+ resetUrl: string;
5744
+ }
5745
+
5746
+ export function ResetPasswordEmail({ resetUrl }: ResetPasswordEmailProps) {
5747
+ return (
5748
+ <Html>
5749
+ <Head />
5750
+ <Body style={{ fontFamily: 'system-ui, sans-serif', background: '#f9fafb' }}>
5751
+ <Container style={{ maxWidth: '600px', margin: '0 auto', padding: '40px 20px' }}>
5752
+ <Heading style={{ fontSize: '24px', color: '#111827' }}>
5753
+ Reset Your Password
5754
+ </Heading>
5755
+ <Text style={{ fontSize: '16px', color: '#4b5563' }}>
5756
+ Click the button below to reset your ${config.pascalCase} password. This link expires in 1 hour.
5757
+ </Text>
5758
+ <Button
5759
+ href={resetUrl}
5760
+ style={{ background: '#2563eb', color: 'white', padding: '12px 24px', borderRadius: '8px', textDecoration: 'none' }}
5761
+ >
5762
+ Reset Password
5763
+ </Button>
5764
+ <Text style={{ fontSize: '14px', color: '#9ca3af', marginTop: '24px' }}>
5765
+ If you didn't request this, you can safely ignore this email.
5766
+ </Text>
5767
+ </Container>
5768
+ </Body>
5769
+ </Html>
5770
+ );
5771
+ }
5772
+ `;
5773
+ }
5774
+ };
5775
+
5776
+ // src/templates/strategies/nodemailer-templates.ts
5777
+ var NodemailerTemplateStrategy = class {
5778
+ packageJson(config) {
5779
+ return `{
5780
+ "name": "@${config.name}/email",
5781
+ "version": "1.0.0",
5782
+ "private": true,
5783
+ "type": "module",
5784
+ "main": "./src/index.ts",
5785
+ "types": "./src/index.ts",
5786
+ "exports": {
5787
+ ".": "./src/index.ts"
5788
+ },
5789
+ "dependencies": {
5790
+ "nodemailer": "${config.versions["nodemailer"] ?? "^6.10.0"}"
5791
+ },
5792
+ "devDependencies": {
5793
+ "@types/nodemailer": "${config.versions["@types/nodemailer"] ?? "^6.4.17"}",
5794
+ "typescript": "${config.versions["typescript"] ?? "^5.8.3"}"
5795
+ }
5796
+ }
5797
+ `;
5798
+ }
5799
+ index(_config) {
5800
+ return `export { sendEmail } from './send.js';
5801
+ export { transporter } from './client.js';
5802
+ `;
5803
+ }
5804
+ client(_config) {
5805
+ return `import nodemailer from 'nodemailer';
5806
+
5807
+ export const transporter = nodemailer.createTransport({
5808
+ host: process.env.SMTP_HOST ?? 'smtp.gmail.com',
5809
+ port: Number(process.env.SMTP_PORT ?? 587),
5810
+ secure: process.env.SMTP_SECURE === 'true',
5811
+ auth: {
5812
+ user: process.env.SMTP_USER,
5813
+ pass: process.env.SMTP_PASS,
5814
+ },
5815
+ });
5816
+ `;
5817
+ }
5818
+ sendFunction(_config) {
5819
+ return `import { transporter } from './client.js';
5820
+
5821
+ interface SendEmailOptions {
5822
+ to: string | string[];
5823
+ subject: string;
5824
+ html: string;
5825
+ text?: string;
5826
+ }
5827
+
5828
+ export async function sendEmail({ to, subject, html, text }: SendEmailOptions) {
5829
+ const info = await transporter.sendMail({
5830
+ from: process.env.EMAIL_FROM ?? 'noreply@example.com',
5831
+ to: Array.isArray(to) ? to.join(', ') : to,
5832
+ subject,
5833
+ html,
5834
+ text,
5835
+ });
5836
+
5837
+ return { messageId: info.messageId };
5838
+ }
5839
+ `;
5840
+ }
5841
+ welcomeTemplate(config) {
5842
+ return `/** Generate welcome email HTML */
5843
+ export function welcomeEmailHtml(name: string): string {
5844
+ const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000';
5845
+
5846
+ return \`<!DOCTYPE html>
5847
+ <html>
5848
+ <body style="font-family: system-ui, sans-serif; background: #f9fafb; padding: 40px 0;">
5849
+ <div style="max-width: 600px; margin: 0 auto; padding: 40px 20px;">
5850
+ <h1 style="font-size: 24px; color: #111827;">Welcome to ${config.pascalCase}!</h1>
5851
+ <p style="font-size: 16px; color: #4b5563;">
5852
+ Hi \${name}, thank you for signing up. We're excited to have you on board.
5853
+ </p>
5854
+ <a href="\${appUrl}" style="display: inline-block; background: #2563eb; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none;">
5855
+ Get Started
5856
+ </a>
5857
+ </div>
5858
+ </body>
5859
+ </html>\`;
5860
+ }
5861
+ `;
5862
+ }
5863
+ resetPasswordTemplate(config) {
5864
+ return `/** Generate password reset email HTML */
5865
+ export function resetPasswordEmailHtml(resetUrl: string): string {
5866
+ return \`<!DOCTYPE html>
5867
+ <html>
5868
+ <body style="font-family: system-ui, sans-serif; background: #f9fafb; padding: 40px 0;">
5869
+ <div style="max-width: 600px; margin: 0 auto; padding: 40px 20px;">
5870
+ <h1 style="font-size: 24px; color: #111827;">Reset Your Password</h1>
5871
+ <p style="font-size: 16px; color: #4b5563;">
5872
+ Click the button below to reset your ${config.pascalCase} password. This link expires in 1 hour.
5873
+ </p>
5874
+ <a href="\${resetUrl}" style="display: inline-block; background: #2563eb; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none;">
5875
+ Reset Password
5876
+ </a>
5877
+ <p style="font-size: 14px; color: #9ca3af; margin-top: 24px;">
5878
+ If you didn't request this, you can safely ignore this email.
5879
+ </p>
5880
+ </div>
5881
+ </body>
5882
+ </html>\`;
5883
+ }
5884
+ `;
5885
+ }
5886
+ };
5887
+
5888
+ // src/templates/strategies/sendgrid-templates.ts
5889
+ var SendGridTemplateStrategy = class {
5890
+ packageJson(config) {
5891
+ return `{
5892
+ "name": "@${config.name}/email",
5893
+ "version": "1.0.0",
5894
+ "private": true,
5895
+ "type": "module",
5896
+ "main": "./src/index.ts",
5897
+ "types": "./src/index.ts",
5898
+ "exports": {
5899
+ ".": "./src/index.ts"
5900
+ },
5901
+ "dependencies": {
5902
+ "@sendgrid/mail": "${config.versions["@sendgrid/mail"] ?? "^8.1.0"}"
5903
+ },
5904
+ "devDependencies": {
5905
+ "typescript": "${config.versions["typescript"] ?? "^5.8.3"}"
5906
+ }
5907
+ }
5908
+ `;
5909
+ }
5910
+ index(_config) {
5911
+ return `export { sendEmail } from './send.js';
5912
+ export { sgMail } from './client.js';
5913
+ `;
5914
+ }
5915
+ client(_config) {
5916
+ return `import sgMail from '@sendgrid/mail';
5917
+
5918
+ sgMail.setApiKey(process.env.SENDGRID_API_KEY ?? '');
5919
+
5920
+ export { sgMail };
5921
+ `;
5922
+ }
5923
+ sendFunction(_config) {
5924
+ return `import { sgMail } from './client.js';
5925
+
5926
+ interface SendEmailOptions {
5927
+ to: string | string[];
5928
+ subject: string;
5929
+ html: string;
5930
+ text?: string;
5931
+ }
5932
+
5933
+ export async function sendEmail({ to, subject, html, text }: SendEmailOptions) {
5934
+ const msg = {
5935
+ to,
5936
+ from: process.env.EMAIL_FROM ?? 'noreply@example.com',
5937
+ subject,
5938
+ html,
5939
+ text: text ?? '',
5940
+ };
5941
+
5942
+ const [response] = await sgMail.send(msg);
5943
+ return { statusCode: response.statusCode };
5944
+ }
5945
+ `;
5946
+ }
5947
+ welcomeTemplate(config) {
5948
+ return `/** Generate welcome email HTML */
5949
+ export function welcomeEmailHtml(name: string): string {
5950
+ const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000';
5951
+
5952
+ return \`<!DOCTYPE html>
5953
+ <html>
5954
+ <body style="font-family: system-ui, sans-serif; background: #f9fafb; padding: 40px 0;">
5955
+ <div style="max-width: 600px; margin: 0 auto; padding: 40px 20px;">
5956
+ <h1 style="font-size: 24px; color: #111827;">Welcome to ${config.pascalCase}!</h1>
5957
+ <p style="font-size: 16px; color: #4b5563;">Hi \${name}, thank you for signing up.</p>
5958
+ <a href="\${appUrl}" style="display: inline-block; background: #2563eb; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none;">Get Started</a>
5959
+ </div>
5960
+ </body>
5961
+ </html>\`;
5962
+ }
5963
+ `;
5964
+ }
5965
+ resetPasswordTemplate(config) {
5966
+ return `/** Generate password reset email HTML */
5967
+ export function resetPasswordEmailHtml(resetUrl: string): string {
5968
+ return \`<!DOCTYPE html>
5969
+ <html>
5970
+ <body style="font-family: system-ui, sans-serif; background: #f9fafb; padding: 40px 0;">
5971
+ <div style="max-width: 600px; margin: 0 auto; padding: 40px 20px;">
5972
+ <h1 style="font-size: 24px; color: #111827;">Reset Your Password</h1>
5973
+ <p style="font-size: 16px; color: #4b5563;">Click below to reset your ${config.pascalCase} password.</p>
5974
+ <a href="\${resetUrl}" style="display: inline-block; background: #2563eb; color: white; padding: 12px 24px; border-radius: 8px; text-decoration: none;">Reset Password</a>
5975
+ </div>
5976
+ </body>
5977
+ </html>\`;
5978
+ }
5979
+ `;
5980
+ }
5981
+ };
5982
+
5983
+ // src/templates/strategies/email-factory.ts
5984
+ function createEmailStrategy(email) {
5985
+ switch (email) {
5986
+ case "resend":
5987
+ return new ResendTemplateStrategy();
5988
+ case "nodemailer":
5989
+ return new NodemailerTemplateStrategy();
5990
+ case "sendgrid":
5991
+ return new SendGridTemplateStrategy();
5992
+ case "none":
5993
+ return null;
5994
+ }
5995
+ }
5996
+
5997
+ // src/templates/strategies/s3-templates.ts
5998
+ var S3TemplateStrategy = class {
5999
+ packageJson(config) {
6000
+ return `{
6001
+ "name": "@${config.name}/storage",
6002
+ "version": "1.0.0",
6003
+ "private": true,
6004
+ "type": "module",
6005
+ "main": "./src/index.ts",
6006
+ "types": "./src/index.ts",
6007
+ "exports": {
6008
+ ".": "./src/index.ts"
6009
+ },
6010
+ "dependencies": {
6011
+ "@aws-sdk/client-s3": "${config.versions["@aws-sdk/client-s3"] ?? "^3.750.0"}",
6012
+ "@aws-sdk/s3-request-presigner": "${config.versions["@aws-sdk/s3-request-presigner"] ?? "^3.750.0"}"
6013
+ },
6014
+ "devDependencies": {
6015
+ "typescript": "${config.versions["typescript"] ?? "^5.8.3"}"
6016
+ }
6017
+ }
6018
+ `;
6019
+ }
6020
+ index(_config) {
6021
+ return `export { s3Client } from './client.js';
6022
+ export { uploadFile, getUploadUrl } from './upload.js';
6023
+ export { getDownloadUrl } from './download.js';
6024
+ `;
6025
+ }
6026
+ client(_config) {
6027
+ return `import { S3Client } from '@aws-sdk/client-s3';
6028
+
6029
+ export const s3Client = new S3Client({
6030
+ region: process.env.AWS_REGION ?? 'us-east-1',
6031
+ credentials: {
6032
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? '',
6033
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? '',
6034
+ },
6035
+ ...(process.env.S3_ENDPOINT ? { endpoint: process.env.S3_ENDPOINT, forcePathStyle: true } : {}),
6036
+ });
6037
+
6038
+ export const BUCKET_NAME = process.env.S3_BUCKET ?? 'uploads';
6039
+ `;
6040
+ }
6041
+ uploadService(_config) {
6042
+ return `import { PutObjectCommand } from '@aws-sdk/client-s3';
6043
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
6044
+ import { s3Client, BUCKET_NAME } from './client.js';
6045
+
6046
+ /** Upload a file buffer directly to S3 */
6047
+ export async function uploadFile(key: string, body: Buffer, contentType: string) {
6048
+ const command = new PutObjectCommand({
6049
+ Bucket: BUCKET_NAME,
6050
+ Key: key,
6051
+ Body: body,
6052
+ ContentType: contentType,
6053
+ });
6054
+
6055
+ await s3Client.send(command);
6056
+ return { key, bucket: BUCKET_NAME };
6057
+ }
6058
+
6059
+ /** Generate a presigned upload URL (client uploads directly to S3) */
6060
+ export async function getUploadUrl(key: string, contentType: string, expiresIn = 3600) {
6061
+ const command = new PutObjectCommand({
6062
+ Bucket: BUCKET_NAME,
6063
+ Key: key,
6064
+ ContentType: contentType,
6065
+ });
6066
+
6067
+ const url = await getSignedUrl(s3Client, command, { expiresIn });
6068
+ return { url, key };
6069
+ }
6070
+ `;
6071
+ }
6072
+ downloadService(_config) {
6073
+ return `import { GetObjectCommand } from '@aws-sdk/client-s3';
6074
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
6075
+ import { s3Client, BUCKET_NAME } from './client.js';
6076
+
6077
+ /** Generate a presigned download URL */
6078
+ export async function getDownloadUrl(key: string, expiresIn = 3600) {
6079
+ const command = new GetObjectCommand({
6080
+ Bucket: BUCKET_NAME,
6081
+ Key: key,
6082
+ });
6083
+
6084
+ return getSignedUrl(s3Client, command, { expiresIn });
6085
+ }
6086
+ `;
6087
+ }
6088
+ apiRoutes(_config) {
6089
+ return {};
6090
+ }
6091
+ };
6092
+
6093
+ // src/templates/strategies/uploadthing-templates.ts
6094
+ var UploadThingTemplateStrategy = class {
6095
+ packageJson(config) {
6096
+ return `{
6097
+ "name": "@${config.name}/storage",
6098
+ "version": "1.0.0",
6099
+ "private": true,
6100
+ "type": "module",
6101
+ "main": "./src/index.ts",
6102
+ "types": "./src/index.ts",
6103
+ "exports": {
6104
+ ".": "./src/index.ts"
6105
+ },
6106
+ "dependencies": {
6107
+ "uploadthing": "${config.versions["uploadthing"] ?? "^7.6.0"}",
6108
+ "@uploadthing/react": "${config.versions["@uploadthing/react"] ?? "^7.3.0"}"
6109
+ },
6110
+ "devDependencies": {
6111
+ "typescript": "${config.versions["typescript"] ?? "^5.8.3"}"
6112
+ }
6113
+ }
6114
+ `;
6115
+ }
6116
+ index(_config) {
6117
+ return `export { utapi } from './client.js';
6118
+ `;
6119
+ }
6120
+ client(_config) {
6121
+ return `import { UTApi } from 'uploadthing/server';
6122
+
6123
+ export const utapi = new UTApi();
6124
+ `;
6125
+ }
6126
+ uploadService(_config) {
6127
+ return `// UploadThing handles uploads via file router (see apps/web/app/api/uploadthing/)
6128
+ // Use the utapi for server-side file operations
6129
+
6130
+ export { utapi } from './client.js';
6131
+ `;
6132
+ }
6133
+ downloadService(_config) {
6134
+ return `import { utapi } from './client.js';
6135
+
6136
+ /** Get file URLs from UploadThing */
6137
+ export async function getFileUrls(fileKeys: string[]) {
6138
+ const response = await utapi.getFileUrls(fileKeys);
6139
+ return response.data;
6140
+ }
6141
+
6142
+ /** Delete files from UploadThing */
6143
+ export async function deleteFiles(fileKeys: string[]) {
6144
+ await utapi.deleteFiles(fileKeys);
6145
+ }
6146
+ `;
6147
+ }
6148
+ apiRoutes(_config) {
6149
+ return {
6150
+ "apps/web/app/api/uploadthing/core.ts": `import { createUploadthing, type FileRouter } from 'uploadthing/next';
6151
+
6152
+ const f = createUploadthing();
6153
+
6154
+ export const ourFileRouter = {
6155
+ imageUploader: f({ image: { maxFileSize: '4MB', maxFileCount: 4 } })
6156
+ .onUploadComplete(async ({ file }) => {
6157
+ console.log('Upload complete:', file.url);
6158
+ return { url: file.url };
6159
+ }),
6160
+
6161
+ documentUploader: f({ pdf: { maxFileSize: '16MB' }, text: { maxFileSize: '1MB' } })
6162
+ .onUploadComplete(async ({ file }) => {
6163
+ console.log('Document uploaded:', file.name);
6164
+ return { url: file.url };
6165
+ }),
6166
+ } satisfies FileRouter;
6167
+
6168
+ export type OurFileRouter = typeof ourFileRouter;
6169
+ `,
6170
+ "apps/web/app/api/uploadthing/route.ts": `import { createRouteHandler } from 'uploadthing/next';
6171
+ import { ourFileRouter } from './core';
6172
+
6173
+ export const { GET, POST } = createRouteHandler({ router: ourFileRouter });
6174
+ `
6175
+ };
6176
+ }
6177
+ };
6178
+
6179
+ // src/templates/strategies/cloudinary-templates.ts
6180
+ var CloudinaryTemplateStrategy = class {
6181
+ packageJson(config) {
6182
+ return `{
6183
+ "name": "@${config.name}/storage",
6184
+ "version": "1.0.0",
6185
+ "private": true,
6186
+ "type": "module",
6187
+ "main": "./src/index.ts",
6188
+ "types": "./src/index.ts",
6189
+ "exports": {
6190
+ ".": "./src/index.ts"
6191
+ },
6192
+ "dependencies": {
6193
+ "cloudinary": "${config.versions["cloudinary"] ?? "^2.6.0"}"
6194
+ },
6195
+ "devDependencies": {
6196
+ "typescript": "${config.versions["typescript"] ?? "^5.8.3"}"
6197
+ }
6198
+ }
6199
+ `;
6200
+ }
6201
+ index(_config) {
6202
+ return `export { cloudinary } from './client.js';
6203
+ export { uploadFile } from './upload.js';
6204
+ export { getOptimizedUrl, getTransformUrl } from './download.js';
6205
+ `;
6206
+ }
6207
+ client(_config) {
6208
+ return `import { v2 as cloudinary } from 'cloudinary';
6209
+
6210
+ cloudinary.config({
6211
+ cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
6212
+ api_key: process.env.CLOUDINARY_API_KEY,
6213
+ api_secret: process.env.CLOUDINARY_API_SECRET,
6214
+ });
6215
+
6216
+ export { cloudinary };
6217
+ `;
6218
+ }
6219
+ uploadService(_config) {
6220
+ return `import { cloudinary } from './client.js';
6221
+
6222
+ interface UploadOptions {
6223
+ folder?: string;
6224
+ resourceType?: 'image' | 'video' | 'raw' | 'auto';
6225
+ transformation?: Record<string, unknown>;
6226
+ }
6227
+
6228
+ /** Upload a file to Cloudinary */
6229
+ export async function uploadFile(
6230
+ filePath: string,
6231
+ options: UploadOptions = {},
6232
+ ) {
6233
+ const result = await cloudinary.uploader.upload(filePath, {
6234
+ folder: options.folder ?? 'uploads',
6235
+ resource_type: options.resourceType ?? 'auto',
6236
+ transformation: options.transformation,
6237
+ });
6238
+
6239
+ return {
6240
+ publicId: result.public_id,
6241
+ url: result.secure_url,
6242
+ format: result.format,
6243
+ width: result.width,
6244
+ height: result.height,
6245
+ bytes: result.bytes,
6246
+ };
6247
+ }
6248
+
6249
+ /** Upload from a buffer */
6250
+ export async function uploadBuffer(
6251
+ buffer: Buffer,
6252
+ options: UploadOptions = {},
6253
+ ): Promise<{ publicId: string; url: string }> {
6254
+ return new Promise((resolve, reject) => {
6255
+ cloudinary.uploader
6256
+ .upload_stream(
6257
+ {
6258
+ folder: options.folder ?? 'uploads',
6259
+ resource_type: options.resourceType ?? 'auto',
6260
+ },
6261
+ (error, result) => {
6262
+ if (error || !result) return reject(error ?? new Error('Upload failed'));
6263
+ resolve({ publicId: result.public_id, url: result.secure_url });
6264
+ },
6265
+ )
6266
+ .end(buffer);
6267
+ });
6268
+ }
6269
+ `;
6270
+ }
6271
+ downloadService(_config) {
6272
+ return `import { cloudinary } from './client.js';
6273
+
6274
+ /** Get an optimized URL for an image */
6275
+ export function getOptimizedUrl(publicId: string, options: { width?: number; height?: number; quality?: string } = {}) {
6276
+ return cloudinary.url(publicId, {
6277
+ fetch_format: 'auto',
6278
+ quality: options.quality ?? 'auto',
6279
+ ...(options.width ? { width: options.width, crop: 'scale' } : {}),
6280
+ ...(options.height ? { height: options.height, crop: 'scale' } : {}),
6281
+ });
6282
+ }
6283
+
6284
+ /** Get a transformed URL */
6285
+ export function getTransformUrl(publicId: string, transformation: Record<string, unknown>) {
6286
+ return cloudinary.url(publicId, transformation);
6287
+ }
6288
+ `;
6289
+ }
6290
+ apiRoutes(_config) {
6291
+ return {};
6292
+ }
6293
+ };
6294
+
6295
+ // src/templates/strategies/storage-factory.ts
6296
+ function createStorageStrategy(storage) {
6297
+ switch (storage) {
6298
+ case "s3":
6299
+ return new S3TemplateStrategy();
6300
+ case "uploadthing":
6301
+ return new UploadThingTemplateStrategy();
6302
+ case "cloudinary":
6303
+ return new CloudinaryTemplateStrategy();
6304
+ case "none":
6305
+ return null;
6306
+ }
6307
+ }
6308
+
6309
+ // src/templates/strategies/stripe-templates.ts
6310
+ var StripeTemplateStrategy = class {
6311
+ packageJson(config) {
6312
+ return `{
6313
+ "name": "@${config.name}/payments",
6314
+ "version": "1.0.0",
6315
+ "private": true,
6316
+ "type": "module",
6317
+ "main": "./src/index.ts",
6318
+ "types": "./src/index.ts",
6319
+ "exports": {
6320
+ ".": "./src/index.ts"
6321
+ },
6322
+ "dependencies": {
6323
+ "stripe": "${config.versions["stripe"] ?? "^17.7.0"}"
6324
+ },
6325
+ "devDependencies": {
6326
+ "typescript": "${config.versions["typescript"] ?? "^5.8.3"}"
6327
+ }
6328
+ }
6329
+ `;
6330
+ }
6331
+ index(_config) {
6332
+ return `export { stripe } from './client.js';
6333
+ export { createCheckoutSession } from './checkout.js';
6334
+ export { handleWebhook } from './webhook.js';
6335
+ export { getSubscription, cancelSubscription } from './subscription.js';
6336
+ `;
6337
+ }
6338
+ client(_config) {
6339
+ return `import Stripe from 'stripe';
6340
+
6341
+ export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY ?? '', {
6342
+ apiVersion: '2025-03-31.basil',
6343
+ typescript: true,
6344
+ });
6345
+ `;
6346
+ }
6347
+ webhookHandler(_config) {
6348
+ return `import type Stripe from 'stripe';
6349
+ import { stripe } from './client.js';
6350
+
6351
+ /** Verify and handle a Stripe webhook event */
6352
+ export async function handleWebhook(body: string | Buffer, signature: string) {
6353
+ const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET ?? '';
6354
+
6355
+ const event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
6356
+
6357
+ switch (event.type) {
6358
+ case 'checkout.session.completed': {
6359
+ const session = event.data.object as Stripe.Checkout.Session;
6360
+ // TODO: Fulfill the order, update database
6361
+ console.log('Checkout completed:', session.id);
6362
+ break;
6363
+ }
6364
+ case 'customer.subscription.updated': {
6365
+ const subscription = event.data.object as Stripe.Subscription;
6366
+ // TODO: Update subscription status in database
6367
+ console.log('Subscription updated:', subscription.id);
6368
+ break;
6369
+ }
6370
+ case 'customer.subscription.deleted': {
6371
+ const subscription = event.data.object as Stripe.Subscription;
6372
+ // TODO: Handle subscription cancellation
6373
+ console.log('Subscription cancelled:', subscription.id);
6374
+ break;
6375
+ }
6376
+ case 'invoice.payment_failed': {
6377
+ const invoice = event.data.object as Stripe.Invoice;
6378
+ // TODO: Notify user of failed payment
6379
+ console.log('Payment failed:', invoice.id);
6380
+ break;
6381
+ }
6382
+ default:
6383
+ console.log('Unhandled event type:', event.type);
6384
+ }
6385
+
6386
+ return { received: true };
6387
+ }
6388
+ `;
6389
+ }
6390
+ checkout(_config) {
6391
+ return `import { stripe } from './client.js';
6392
+
6393
+ interface CreateCheckoutOptions {
6394
+ priceId: string;
6395
+ customerId?: string;
6396
+ customerEmail?: string;
6397
+ mode?: 'payment' | 'subscription';
6398
+ successUrl?: string;
6399
+ cancelUrl?: string;
6400
+ }
6401
+
6402
+ /** Create a Stripe Checkout session */
6403
+ export async function createCheckoutSession({
6404
+ priceId,
6405
+ customerId,
6406
+ customerEmail,
6407
+ mode = 'subscription',
6408
+ successUrl,
6409
+ cancelUrl,
6410
+ }: CreateCheckoutOptions) {
6411
+ const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000';
6412
+
6413
+ const session = await stripe.checkout.sessions.create({
6414
+ mode,
6415
+ payment_method_types: ['card'],
6416
+ line_items: [{ price: priceId, quantity: 1 }],
6417
+ ...(customerId ? { customer: customerId } : {}),
6418
+ ...(customerEmail && !customerId ? { customer_email: customerEmail } : {}),
6419
+ success_url: successUrl ?? \`\${appUrl}/checkout/success?session_id={CHECKOUT_SESSION_ID}\`,
6420
+ cancel_url: cancelUrl ?? \`\${appUrl}/pricing\`,
6421
+ });
6422
+
6423
+ return { sessionId: session.id, url: session.url };
6424
+ }
6425
+ `;
6426
+ }
6427
+ subscription(_config) {
6428
+ return `import { stripe } from './client.js';
6429
+
6430
+ /** Get a customer's active subscription */
6431
+ export async function getSubscription(customerId: string) {
6432
+ const subscriptions = await stripe.subscriptions.list({
6433
+ customer: customerId,
6434
+ status: 'active',
6435
+ limit: 1,
6436
+ });
6437
+
6438
+ return subscriptions.data[0] ?? null;
6439
+ }
6440
+
6441
+ /** Cancel a subscription at period end */
6442
+ export async function cancelSubscription(subscriptionId: string) {
6443
+ return stripe.subscriptions.update(subscriptionId, {
6444
+ cancel_at_period_end: true,
6445
+ });
6446
+ }
6447
+
6448
+ /** Create a customer portal session */
6449
+ export async function createPortalSession(customerId: string, returnUrl?: string) {
6450
+ const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000';
6451
+
6452
+ const session = await stripe.billingPortal.sessions.create({
6453
+ customer: customerId,
6454
+ return_url: returnUrl ?? \`\${appUrl}/dashboard\`,
6455
+ });
6456
+
6457
+ return { url: session.url };
6458
+ }
6459
+ `;
6460
+ }
6461
+ pricingPage(config) {
6462
+ return `import { Button } from '@${config.name}/ui';
6463
+
6464
+ const plans = [
6465
+ {
6466
+ name: 'Free',
6467
+ price: '$0',
6468
+ period: '/month',
6469
+ features: ['1 project', 'Basic support', 'Community access'],
6470
+ priceId: '', // No checkout for free tier
6471
+ highlighted: false,
6472
+ },
6473
+ {
6474
+ name: 'Pro',
6475
+ price: '$19',
6476
+ period: '/month',
6477
+ features: ['Unlimited projects', 'Priority support', 'Advanced features', 'API access'],
6478
+ priceId: process.env.NEXT_PUBLIC_STRIPE_PRO_PRICE_ID ?? '',
6479
+ highlighted: true,
6480
+ },
6481
+ {
6482
+ name: 'Enterprise',
6483
+ price: '$99',
6484
+ period: '/month',
6485
+ features: ['Everything in Pro', 'Dedicated support', 'Custom integrations', 'SLA guarantee'],
6486
+ priceId: process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PRICE_ID ?? '',
6487
+ highlighted: false,
6488
+ },
6489
+ ];
6490
+
6491
+ export default function PricingPage() {
6492
+ return (
6493
+ <main style={{ minHeight: '100vh', padding: '4rem 2rem' }}>
6494
+ <div style={{ maxWidth: '1024px', margin: '0 auto', textAlign: 'center' }}>
6495
+ <h1 style={{ fontSize: '2.5rem', fontWeight: 'bold', marginBottom: '1rem' }}>
6496
+ Simple Pricing
6497
+ </h1>
6498
+ <p style={{ fontSize: '1.25rem', color: '#64748b', marginBottom: '3rem' }}>
6499
+ Choose the plan that fits your needs
6500
+ </p>
6501
+
6502
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '1.5rem' }}>
6503
+ {plans.map((plan) => (
6504
+ <div
6505
+ key={plan.name}
6506
+ style={{
6507
+ border: plan.highlighted ? '2px solid #2563eb' : '1px solid #e2e8f0',
6508
+ borderRadius: '12px',
6509
+ padding: '2rem',
6510
+ background: 'white',
6511
+ }}
6512
+ >
6513
+ <h3 style={{ fontSize: '1.25rem', fontWeight: '600' }}>{plan.name}</h3>
6514
+ <div style={{ margin: '1rem 0' }}>
6515
+ <span style={{ fontSize: '2.5rem', fontWeight: 'bold' }}>{plan.price}</span>
6516
+ <span style={{ color: '#64748b' }}>{plan.period}</span>
6517
+ </div>
6518
+ <ul style={{ listStyle: 'none', padding: 0, margin: '1.5rem 0', textAlign: 'left' }}>
6519
+ {plan.features.map((feature) => (
6520
+ <li key={feature} style={{ padding: '0.5rem 0', color: '#374151' }}>
6521
+ \u2713 {feature}
6522
+ </li>
6523
+ ))}
6524
+ </ul>
6525
+ <Button
6526
+ variant={plan.highlighted ? 'primary' : 'outline'}
6527
+ style={{ width: '100%' }}
6528
+ >
6529
+ {plan.priceId ? 'Get Started' : 'Start Free'}
6530
+ </Button>
6531
+ </div>
6532
+ ))}
6533
+ </div>
6534
+ </div>
6535
+ </main>
6536
+ );
6537
+ }
6538
+ `;
6539
+ }
6540
+ apiRoute(config) {
6541
+ if (config.backend === "nestjs") {
6542
+ return `import { Controller, Post, Req, Headers, RawBodyRequest } from '@nestjs/common';
6543
+ import type { Request } from 'express';
6544
+ import { handleWebhook } from '@${config.name}/payments';
6545
+
6546
+ @Controller('payments')
6547
+ export class PaymentsController {
6548
+ @Post('webhook')
6549
+ async webhook(
6550
+ @Req() req: RawBodyRequest<Request>,
6551
+ @Headers('stripe-signature') signature: string,
6552
+ ) {
6553
+ const rawBody = req.rawBody;
6554
+ if (!rawBody) throw new Error('Missing raw body');
6555
+ return handleWebhook(rawBody, signature);
6556
+ }
6557
+ }
6558
+ `;
6559
+ }
6560
+ return `import { Router } from 'express';
6561
+ import { handleWebhook } from '@${config.name}/payments';
6562
+
6563
+ export const paymentsRouter = Router();
6564
+
6565
+ paymentsRouter.post('/payments/webhook', async (req, res, next) => {
6566
+ try {
6567
+ const signature = req.headers['stripe-signature'] as string;
6568
+ const result = await handleWebhook(req.body, signature);
6569
+ res.json(result);
6570
+ } catch (error) {
6571
+ next(error);
6572
+ }
6573
+ });
6574
+ `;
6575
+ }
6576
+ };
6577
+
6578
+ // src/templates/strategies/lemonsqueezy-templates.ts
6579
+ var LemonSqueezyTemplateStrategy = class {
6580
+ packageJson(config) {
6581
+ return `{
6582
+ "name": "@${config.name}/payments",
6583
+ "version": "1.0.0",
6584
+ "private": true,
6585
+ "type": "module",
6586
+ "main": "./src/index.ts",
6587
+ "types": "./src/index.ts",
6588
+ "dependencies": {
6589
+ "@lemonsqueezy/lemonsqueezy.js": "${config.versions["@lemonsqueezy/lemonsqueezy.js"] ?? "^4.0.0"}"
6590
+ },
6591
+ "devDependencies": {
6592
+ "typescript": "${config.versions["typescript"] ?? "^5.8.3"}"
6593
+ }
6594
+ }
6595
+ `;
6596
+ }
6597
+ index(_config) {
6598
+ return `export { lemonSqueezySetup } from './client.js';
6599
+ export { createCheckout } from './checkout.js';
6600
+ export { handleWebhook } from './webhook.js';
6601
+ export { getSubscription, cancelSubscription } from './subscription.js';
6602
+ `;
6603
+ }
6604
+ client(_config) {
6605
+ return `import { lemonSqueezySetup as setup } from '@lemonsqueezy/lemonsqueezy.js';
6606
+
6607
+ export function lemonSqueezySetup() {
6608
+ setup({ apiKey: process.env.LEMONSQUEEZY_API_KEY ?? '', onError: (error) => console.error('LemonSqueezy error:', error) });
6609
+ }
6610
+ `;
6611
+ }
6612
+ webhookHandler(_config) {
6613
+ return `import crypto from 'node:crypto';
6614
+
6615
+ /** Verify LemonSqueezy webhook signature and parse event */
6616
+ export async function handleWebhook(body: string, signature: string) {
6617
+ const secret = process.env.LEMONSQUEEZY_WEBHOOK_SECRET ?? '';
6618
+ const hmac = crypto.createHmac('sha256', secret);
6619
+ const digest = hmac.update(body).digest('hex');
6620
+
6621
+ if (digest !== signature) {
6622
+ throw new Error('Invalid webhook signature');
6623
+ }
6624
+
6625
+ const event = JSON.parse(body);
6626
+ const eventName = event.meta?.event_name;
6627
+
6628
+ switch (eventName) {
6629
+ case 'order_created':
6630
+ console.log('Order created:', event.data?.id);
6631
+ break;
6632
+ case 'subscription_created':
6633
+ console.log('Subscription created:', event.data?.id);
6634
+ break;
6635
+ case 'subscription_cancelled':
6636
+ console.log('Subscription cancelled:', event.data?.id);
6637
+ break;
6638
+ case 'subscription_payment_failed':
6639
+ console.log('Payment failed:', event.data?.id);
6640
+ break;
6641
+ default:
6642
+ console.log('Unhandled event:', eventName);
6643
+ }
6644
+
6645
+ return { received: true };
6646
+ }
6647
+ `;
6648
+ }
6649
+ checkout(_config) {
6650
+ return `import { createCheckout as lsCreateCheckout } from '@lemonsqueezy/lemonsqueezy.js';
6651
+ import { lemonSqueezySetup } from './client.js';
6652
+
6653
+ interface CreateCheckoutOptions {
6654
+ variantId: number;
6655
+ email?: string;
6656
+ name?: string;
6657
+ }
6658
+
6659
+ /** Create a LemonSqueezy checkout URL */
6660
+ export async function createCheckout({ variantId, email, name }: CreateCheckoutOptions) {
6661
+ lemonSqueezySetup();
6662
+
6663
+ const storeId = process.env.LEMONSQUEEZY_STORE_ID ?? '';
6664
+ const appUrl = process.env.NEXT_PUBLIC_APP_URL ?? 'http://localhost:3000';
6665
+
6666
+ const { data } = await lsCreateCheckout(storeId, variantId, {
6667
+ checkoutData: {
6668
+ email,
6669
+ name,
6670
+ },
6671
+ productOptions: {
6672
+ redirectUrl: \`\${appUrl}/checkout/success\`,
6673
+ },
6674
+ });
6675
+
6676
+ return { url: data?.data?.attributes?.url ?? null };
6677
+ }
6678
+ `;
6679
+ }
6680
+ subscription(_config) {
6681
+ return `import { getSubscription as lsGetSubscription, cancelSubscription as lsCancelSubscription } from '@lemonsqueezy/lemonsqueezy.js';
6682
+ import { lemonSqueezySetup } from './client.js';
6683
+
6684
+ /** Get subscription details */
6685
+ export async function getSubscription(subscriptionId: string) {
6686
+ lemonSqueezySetup();
6687
+ const { data } = await lsGetSubscription(subscriptionId);
6688
+ return data?.data ?? null;
6689
+ }
6690
+
6691
+ /** Cancel a subscription */
6692
+ export async function cancelSubscription(subscriptionId: string) {
6693
+ lemonSqueezySetup();
6694
+ const { data } = await lsCancelSubscription(subscriptionId);
6695
+ return data?.data ?? null;
6696
+ }
6697
+ `;
6698
+ }
6699
+ pricingPage(config) {
6700
+ return `import { Button } from '@${config.name}/ui';
6701
+
6702
+ const plans = [
6703
+ { name: 'Free', price: '$0', period: '/month', features: ['1 project', 'Basic support'], variantId: 0, highlighted: false },
6704
+ { name: 'Pro', price: '$19', period: '/month', features: ['Unlimited projects', 'Priority support', 'Advanced features'], variantId: Number(process.env.NEXT_PUBLIC_LS_PRO_VARIANT_ID ?? 0), highlighted: true },
6705
+ { name: 'Enterprise', price: '$99', period: '/month', features: ['Everything in Pro', 'Dedicated support', 'SLA'], variantId: Number(process.env.NEXT_PUBLIC_LS_ENTERPRISE_VARIANT_ID ?? 0), highlighted: false },
6706
+ ];
6707
+
6708
+ export default function PricingPage() {
6709
+ return (
6710
+ <main style={{ minHeight: '100vh', padding: '4rem 2rem' }}>
6711
+ <div style={{ maxWidth: '1024px', margin: '0 auto', textAlign: 'center' }}>
6712
+ <h1 style={{ fontSize: '2.5rem', fontWeight: 'bold', marginBottom: '1rem' }}>Pricing</h1>
6713
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '1.5rem' }}>
6714
+ {plans.map((plan) => (
6715
+ <div key={plan.name} style={{ border: plan.highlighted ? '2px solid #2563eb' : '1px solid #e2e8f0', borderRadius: '12px', padding: '2rem', background: 'white' }}>
6716
+ <h3 style={{ fontSize: '1.25rem', fontWeight: '600' }}>{plan.name}</h3>
6717
+ <div style={{ margin: '1rem 0' }}>
6718
+ <span style={{ fontSize: '2.5rem', fontWeight: 'bold' }}>{plan.price}</span>
6719
+ <span style={{ color: '#64748b' }}>{plan.period}</span>
6720
+ </div>
6721
+ <ul style={{ listStyle: 'none', padding: 0, margin: '1.5rem 0', textAlign: 'left' }}>
6722
+ {plan.features.map((f) => <li key={f} style={{ padding: '0.5rem 0' }}>\u2713 {f}</li>)}
6723
+ </ul>
6724
+ <Button variant={plan.highlighted ? 'primary' : 'outline'} style={{ width: '100%' }}>
6725
+ {plan.variantId ? 'Get Started' : 'Start Free'}
6726
+ </Button>
6727
+ </div>
6728
+ ))}
6729
+ </div>
6730
+ </div>
6731
+ </main>
6732
+ );
6733
+ }
6734
+ `;
6735
+ }
6736
+ apiRoute(config) {
6737
+ if (config.backend === "nestjs") {
6738
+ return `import { Controller, Post, Req, Headers } from '@nestjs/common';
6739
+ import type { Request } from 'express';
6740
+ import { handleWebhook } from '@${config.name}/payments';
6741
+
6742
+ @Controller('payments')
6743
+ export class PaymentsController {
6744
+ @Post('webhook')
6745
+ async webhook(@Req() req: Request, @Headers('x-signature') signature: string) {
6746
+ const body = JSON.stringify(req.body);
6747
+ return handleWebhook(body, signature);
6748
+ }
6749
+ }
6750
+ `;
6751
+ }
6752
+ return `import { Router } from 'express';
6753
+ import { handleWebhook } from '@${config.name}/payments';
6754
+
6755
+ export const paymentsRouter = Router();
6756
+
6757
+ paymentsRouter.post('/payments/webhook', async (req, res, next) => {
6758
+ try {
6759
+ const signature = req.headers['x-signature'] as string;
6760
+ const result = await handleWebhook(JSON.stringify(req.body), signature);
6761
+ res.json(result);
6762
+ } catch (error) {
6763
+ next(error);
6764
+ }
6765
+ });
6766
+ `;
6767
+ }
6768
+ };
6769
+
6770
+ // src/templates/strategies/paddle-templates.ts
6771
+ var PaddleTemplateStrategy = class {
6772
+ packageJson(config) {
6773
+ return `{
6774
+ "name": "@${config.name}/payments",
6775
+ "version": "1.0.0",
6776
+ "private": true,
6777
+ "type": "module",
6778
+ "main": "./src/index.ts",
6779
+ "types": "./src/index.ts",
6780
+ "dependencies": {
6781
+ "@paddle/paddle-node-sdk": "${config.versions["@paddle/paddle-node-sdk"] ?? "^1.7.0"}"
6782
+ },
6783
+ "devDependencies": {
6784
+ "typescript": "${config.versions["typescript"] ?? "^5.8.3"}"
6785
+ }
6786
+ }
6787
+ `;
6788
+ }
6789
+ index(_config) {
6790
+ return `export { paddle } from './client.js';
6791
+ export { createCheckout } from './checkout.js';
6792
+ export { handleWebhook } from './webhook.js';
6793
+ export { getSubscription, cancelSubscription } from './subscription.js';
6794
+ `;
6795
+ }
6796
+ client(_config) {
6797
+ return `import { Paddle, Environment } from '@paddle/paddle-node-sdk';
6798
+
6799
+ const isProd = process.env.NODE_ENV === 'production';
6800
+
6801
+ export const paddle = new Paddle(process.env.PADDLE_API_KEY ?? '', {
6802
+ environment: isProd ? Environment.production : Environment.sandbox,
6803
+ });
6804
+ `;
6805
+ }
6806
+ webhookHandler(_config) {
6807
+ return `import { paddle } from './client.js';
6808
+
6809
+ /** Verify and handle a Paddle webhook event */
6810
+ export async function handleWebhook(body: string, signature: string) {
6811
+ const secretKey = process.env.PADDLE_WEBHOOK_SECRET ?? '';
6812
+
6813
+ const eventData = paddle.webhooks.unmarshal(body, secretKey, signature);
6814
+ if (!eventData) throw new Error('Invalid webhook signature');
6815
+
6816
+ switch (eventData.eventType) {
6817
+ case 'transaction.completed':
6818
+ console.log('Transaction completed:', eventData.data?.id);
6819
+ break;
6820
+ case 'subscription.activated':
6821
+ console.log('Subscription activated:', eventData.data?.id);
6822
+ break;
6823
+ case 'subscription.canceled':
6824
+ console.log('Subscription cancelled:', eventData.data?.id);
6825
+ break;
6826
+ case 'subscription.past_due':
6827
+ console.log('Subscription past due:', eventData.data?.id);
6828
+ break;
6829
+ default:
6830
+ console.log('Unhandled event:', eventData.eventType);
6831
+ }
6832
+
6833
+ return { received: true };
6834
+ }
6835
+ `;
6836
+ }
6837
+ checkout(_config) {
6838
+ return `import { paddle } from './client.js';
6839
+
6840
+ interface CreateCheckoutOptions {
6841
+ priceId: string;
6842
+ customerId?: string;
6843
+ customerEmail?: string;
6844
+ }
4284
6845
 
4285
- if (typeof window === 'undefined') return 'light';
4286
- return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
4287
- });
6846
+ /** Create a Paddle checkout transaction */
6847
+ export async function createCheckout({ priceId, customerId, customerEmail }: CreateCheckoutOptions) {
6848
+ const transaction = await paddle.transactions.create({
6849
+ items: [{ priceId, quantity: 1 }],
6850
+ ...(customerId ? { customerId } : {}),
6851
+ ...(customerEmail ? { customerEmail } : {}),
6852
+ });
4288
6853
 
4289
- /** Convenience hook for theme management */
4290
- export function useTheme() {
4291
- const [theme, setTheme] = useAtom(themeAtom);
4292
- return { theme, setTheme };
6854
+ return { transactionId: transaction.id };
4293
6855
  }
4294
6856
  `;
4295
6857
  }
4296
- /** Jotai works without a provider (uses default store) */
4297
- providerWrapper() {
4298
- return "";
6858
+ subscription(_config) {
6859
+ return `import { paddle } from './client.js';
6860
+
6861
+ /** Get subscription details */
6862
+ export async function getSubscription(subscriptionId: string) {
6863
+ return paddle.subscriptions.get(subscriptionId);
6864
+ }
6865
+
6866
+ /** Cancel a subscription */
6867
+ export async function cancelSubscription(subscriptionId: string) {
6868
+ return paddle.subscriptions.cancel(subscriptionId, { effectiveFrom: 'next_billing_period' });
6869
+ }
6870
+ `;
4299
6871
  }
4300
- };
6872
+ pricingPage(config) {
6873
+ return `import { Button } from '@${config.name}/ui';
4301
6874
 
4302
- // src/templates/strategies/redux-templates.ts
4303
- var ReduxTemplateStrategy = class {
4304
- storeSetup() {
4305
- return `import { configureStore } from '@reduxjs/toolkit';
4306
- import { useDispatch, useSelector } from 'react-redux';
4307
- import type { TypedUseSelectorHook } from 'react-redux';
4308
- import { themeReducer } from './theme-slice.js';
6875
+ const plans = [
6876
+ { name: 'Free', price: '$0', period: '/month', features: ['1 project', 'Basic support'], priceId: '', highlighted: false },
6877
+ { name: 'Pro', price: '$19', period: '/month', features: ['Unlimited projects', 'Priority support', 'Advanced features'], priceId: process.env.NEXT_PUBLIC_PADDLE_PRO_PRICE_ID ?? '', highlighted: true },
6878
+ { name: 'Enterprise', price: '$99', period: '/month', features: ['Everything in Pro', 'Dedicated support', 'SLA'], priceId: process.env.NEXT_PUBLIC_PADDLE_ENTERPRISE_PRICE_ID ?? '', highlighted: false },
6879
+ ];
4309
6880
 
4310
- export const store = configureStore({
4311
- reducer: {
4312
- theme: themeReducer,
4313
- },
4314
- });
6881
+ export default function PricingPage() {
6882
+ return (
6883
+ <main style={{ minHeight: '100vh', padding: '4rem 2rem' }}>
6884
+ <div style={{ maxWidth: '1024px', margin: '0 auto', textAlign: 'center' }}>
6885
+ <h1 style={{ fontSize: '2.5rem', fontWeight: 'bold', marginBottom: '1rem' }}>Pricing</h1>
6886
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))', gap: '1.5rem' }}>
6887
+ {plans.map((plan) => (
6888
+ <div key={plan.name} style={{ border: plan.highlighted ? '2px solid #2563eb' : '1px solid #e2e8f0', borderRadius: '12px', padding: '2rem', background: 'white' }}>
6889
+ <h3 style={{ fontSize: '1.25rem', fontWeight: '600' }}>{plan.name}</h3>
6890
+ <div style={{ margin: '1rem 0' }}>
6891
+ <span style={{ fontSize: '2.5rem', fontWeight: 'bold' }}>{plan.price}</span>
6892
+ <span style={{ color: '#64748b' }}>{plan.period}</span>
6893
+ </div>
6894
+ <ul style={{ listStyle: 'none', padding: 0, margin: '1.5rem 0', textAlign: 'left' }}>
6895
+ {plan.features.map((f) => <li key={f} style={{ padding: '0.5rem 0' }}>\u2713 {f}</li>)}
6896
+ </ul>
6897
+ <Button variant={plan.highlighted ? 'primary' : 'outline'} style={{ width: '100%' }}>
6898
+ {plan.priceId ? 'Get Started' : 'Start Free'}
6899
+ </Button>
6900
+ </div>
6901
+ ))}
6902
+ </div>
6903
+ </div>
6904
+ </main>
6905
+ );
6906
+ }
6907
+ `;
6908
+ }
6909
+ apiRoute(config) {
6910
+ if (config.backend === "nestjs") {
6911
+ return `import { Controller, Post, Req, Headers } from '@nestjs/common';
6912
+ import type { Request } from 'express';
6913
+ import { handleWebhook } from '@${config.name}/payments';
4315
6914
 
4316
- export type RootState = ReturnType<typeof store.getState>;
4317
- export type AppDispatch = typeof store.dispatch;
6915
+ @Controller('payments')
6916
+ export class PaymentsController {
6917
+ @Post('webhook')
6918
+ async webhook(@Req() req: Request, @Headers('paddle-signature') signature: string) {
6919
+ const body = JSON.stringify(req.body);
6920
+ return handleWebhook(body, signature);
6921
+ }
6922
+ }
6923
+ `;
6924
+ }
6925
+ return `import { Router } from 'express';
6926
+ import { handleWebhook } from '@${config.name}/payments';
4318
6927
 
4319
- /** Typed dispatch hook */
4320
- export const useAppDispatch: () => AppDispatch = useDispatch;
6928
+ export const paymentsRouter = Router();
4321
6929
 
4322
- /** Typed selector hook */
4323
- export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
6930
+ paymentsRouter.post('/payments/webhook', async (req, res, next) => {
6931
+ try {
6932
+ const signature = req.headers['paddle-signature'] as string;
6933
+ const result = await handleWebhook(JSON.stringify(req.body), signature);
6934
+ res.json(result);
6935
+ } catch (error) {
6936
+ next(error);
6937
+ }
6938
+ });
4324
6939
  `;
4325
6940
  }
4326
- exampleStore() {
4327
- return `import { createSlice } from '@reduxjs/toolkit';
4328
- import type { PayloadAction } from '@reduxjs/toolkit';
4329
-
4330
- type Theme = 'light' | 'dark' | 'system';
6941
+ };
4331
6942
 
4332
- interface ThemeState {
4333
- theme: Theme;
6943
+ // src/templates/strategies/payment-factory.ts
6944
+ function createPaymentStrategy(payments) {
6945
+ switch (payments) {
6946
+ case "stripe":
6947
+ return new StripeTemplateStrategy();
6948
+ case "lemonsqueezy":
6949
+ return new LemonSqueezyTemplateStrategy();
6950
+ case "paddle":
6951
+ return new PaddleTemplateStrategy();
6952
+ case "none":
6953
+ return null;
6954
+ }
4334
6955
  }
4335
6956
 
4336
- const initialState: ThemeState = {
4337
- theme: 'system',
4338
- };
6957
+ // src/templates/strategies/swagger-templates.ts
6958
+ var SwaggerTemplateStrategy = class {
6959
+ docsConfig(config) {
6960
+ if (config.backend === "nestjs") {
6961
+ return `import type { INestApplication } from '@nestjs/common';
6962
+ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
4339
6963
 
4340
- const themeSlice = createSlice({
4341
- name: 'theme',
4342
- initialState,
4343
- reducers: {
4344
- setTheme(state, action: PayloadAction<Theme>) {
4345
- state.theme = action.payload;
6964
+ export function setupSwagger(app: INestApplication) {
6965
+ const config = new DocumentBuilder()
6966
+ .setTitle('${config.pascalCase} API')
6967
+ .setDescription('${config.pascalCase} REST API documentation')
6968
+ .setVersion('1.0.0')
6969
+ .addBearerAuth()
6970
+ .build();
6971
+
6972
+ const document = SwaggerModule.createDocument(app, config);
6973
+ SwaggerModule.setup('api/docs', app, document, {
6974
+ swaggerOptions: {
6975
+ persistAuthorization: true,
4346
6976
  },
4347
- toggleTheme(state) {
4348
- state.theme = state.theme === 'dark' ? 'light' : 'dark';
6977
+ });
6978
+ }
6979
+ `;
6980
+ }
6981
+ return `import swaggerJsdoc from 'swagger-jsdoc';
6982
+
6983
+ const options: swaggerJsdoc.Options = {
6984
+ definition: {
6985
+ openapi: '3.0.0',
6986
+ info: {
6987
+ title: '${config.pascalCase} API',
6988
+ description: '${config.pascalCase} REST API documentation',
6989
+ version: '1.0.0',
6990
+ },
6991
+ servers: [
6992
+ { url: 'http://localhost:3001', description: 'Development' },
6993
+ ],
6994
+ components: {
6995
+ securitySchemes: {
6996
+ bearerAuth: {
6997
+ type: 'http',
6998
+ scheme: 'bearer',
6999
+ bearerFormat: 'JWT',
7000
+ },
7001
+ },
4349
7002
  },
4350
7003
  },
4351
- });
7004
+ apis: ['./src/routes/*.ts'],
7005
+ };
4352
7006
 
4353
- export const { setTheme, toggleTheme } = themeSlice.actions;
4354
- export const themeReducer = themeSlice.reducer;
7007
+ export const swaggerSpec = swaggerJsdoc(options);
4355
7008
  `;
4356
7009
  }
4357
- providerWrapper() {
4358
- return `'use client';
4359
-
4360
- import { Provider } from 'react-redux';
4361
- import { store } from './store';
4362
-
4363
- export function StoreProvider({ children }: { children: React.ReactNode }) {
4364
- return <Provider store={store}>{children}</Provider>;
7010
+ docsSetup(config) {
7011
+ if (config.backend === "nestjs") {
7012
+ return `// Add to apps/api/src/main.ts:
7013
+ // import { setupSwagger } from './docs/swagger-config.js';
7014
+ // setupSwagger(app);
7015
+ //
7016
+ // Swagger UI will be available at: http://localhost:3001/api/docs
7017
+ `;
7018
+ }
7019
+ return `import swaggerUi from 'swagger-ui-express';
7020
+ import type { Express } from 'express';
7021
+ import { swaggerSpec } from './swagger-config.js';
7022
+
7023
+ export function setupDocs(app: Express) {
7024
+ app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec, {
7025
+ swaggerOptions: {
7026
+ persistAuthorization: true,
7027
+ },
7028
+ }));
4365
7029
  }
4366
7030
  `;
4367
7031
  }
4368
7032
  };
4369
7033
 
4370
- // src/templates/strategies/tanstack-query-templates.ts
4371
- var TanstackQueryTemplateStrategy = class {
4372
- storeSetup() {
4373
- return `/**
4374
- * TanStack Query setup and utilities.
4375
- */
7034
+ // src/templates/strategies/redoc-templates.ts
7035
+ var RedocTemplateStrategy = class {
7036
+ docsConfig(config) {
7037
+ if (config.backend === "nestjs") {
7038
+ return `import type { INestApplication } from '@nestjs/common';
7039
+ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
4376
7040
 
4377
- export { queryClient } from './query-client.js';
4378
- export { QueryProvider } from './provider.js';
4379
- `;
4380
- }
4381
- exampleStore(_config) {
4382
- return `import { QueryClient } from '@tanstack/react-query';
7041
+ export function getOpenApiDocument(app: INestApplication) {
7042
+ const config = new DocumentBuilder()
7043
+ .setTitle('${config.pascalCase} API')
7044
+ .setDescription('${config.pascalCase} REST API documentation')
7045
+ .setVersion('1.0.0')
7046
+ .addBearerAuth()
7047
+ .build();
4383
7048
 
4384
- export const queryClient = new QueryClient({
4385
- defaultOptions: {
4386
- queries: {
4387
- staleTime: 60 * 1000, // 1 minute
4388
- retry: 1,
4389
- refetchOnWindowFocus: false,
4390
- },
4391
- mutations: {
4392
- retry: 0,
7049
+ return SwaggerModule.createDocument(app, config);
7050
+ }
7051
+ `;
7052
+ }
7053
+ return `import swaggerJsdoc from 'swagger-jsdoc';
7054
+
7055
+ const options: swaggerJsdoc.Options = {
7056
+ definition: {
7057
+ openapi: '3.0.0',
7058
+ info: {
7059
+ title: '${config.pascalCase} API',
7060
+ description: '${config.pascalCase} REST API documentation',
7061
+ version: '1.0.0',
4393
7062
  },
7063
+ servers: [
7064
+ { url: 'http://localhost:3001', description: 'Development' },
7065
+ ],
4394
7066
  },
4395
- });
7067
+ apis: ['./src/routes/*.ts'],
7068
+ };
4396
7069
 
4397
- /**
4398
- * Example query hook \u2014 fetches health status from the API.
4399
- */
4400
- export function useHealthQuery() {
4401
- const apiUrl = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3001';
7070
+ export const swaggerSpec = swaggerJsdoc(options);
7071
+ `;
7072
+ }
7073
+ docsSetup(config) {
7074
+ if (config.backend === "nestjs") {
7075
+ return `import type { INestApplication } from '@nestjs/common';
7076
+ import redoc from 'redoc-express';
7077
+ import { getOpenApiDocument } from './swagger-config.js';
4402
7078
 
4403
- return {
4404
- queryKey: ['health'],
4405
- queryFn: async () => {
4406
- const response = await fetch(\`\${apiUrl}/api/health\`);
4407
- if (!response.ok) throw new Error('API health check failed');
4408
- return response.json();
4409
- },
4410
- };
7079
+ export function setupDocs(app: INestApplication) {
7080
+ const httpAdapter = app.getHttpAdapter();
7081
+ const document = getOpenApiDocument(app);
7082
+
7083
+ httpAdapter.get('/api/docs/openapi.json', (_req: any, res: any) => {
7084
+ res.json(document);
7085
+ });
7086
+
7087
+ httpAdapter.use(
7088
+ '/api/docs',
7089
+ redoc({
7090
+ title: '${config.pascalCase} API',
7091
+ specUrl: '/api/docs/openapi.json',
7092
+ }),
7093
+ );
4411
7094
  }
4412
7095
  `;
4413
- }
4414
- providerWrapper() {
4415
- return `'use client';
7096
+ }
7097
+ return `import redoc from 'redoc-express';
7098
+ import type { Express } from 'express';
7099
+ import { swaggerSpec } from './swagger-config.js';
4416
7100
 
4417
- import { QueryClientProvider } from '@tanstack/react-query';
4418
- import { queryClient } from './query-client';
7101
+ export function setupDocs(app: Express) {
7102
+ app.get('/api/docs/openapi.json', (_req, res) => {
7103
+ res.json(swaggerSpec);
7104
+ });
4419
7105
 
4420
- export function QueryProvider({ children }: { children: React.ReactNode }) {
4421
- return (
4422
- <QueryClientProvider client={queryClient}>
4423
- {children}
4424
- </QueryClientProvider>
7106
+ app.use(
7107
+ '/api/docs',
7108
+ redoc({
7109
+ title: '${config.pascalCase} API',
7110
+ specUrl: '/api/docs/openapi.json',
7111
+ }),
4425
7112
  );
4426
7113
  }
4427
7114
  `;
4428
7115
  }
4429
7116
  };
4430
7117
 
4431
- // src/templates/strategies/state-factory.ts
4432
- function createStateStrategy(state) {
4433
- switch (state) {
4434
- case "zustand":
4435
- return new ZustandTemplateStrategy();
4436
- case "jotai":
4437
- return new JotaiTemplateStrategy();
4438
- case "redux":
4439
- return new ReduxTemplateStrategy();
4440
- case "tanstack-query":
4441
- return new TanstackQueryTemplateStrategy();
7118
+ // src/templates/strategies/api-docs-factory.ts
7119
+ function createApiDocsStrategy(apiDocs) {
7120
+ switch (apiDocs) {
7121
+ case "swagger":
7122
+ return new SwaggerTemplateStrategy();
7123
+ case "redoc":
7124
+ return new RedocTemplateStrategy();
4442
7125
  case "none":
4443
7126
  return null;
4444
7127
  }
4445
7128
  }
4446
7129
 
4447
7130
  // src/generator.ts
4448
- var TOTAL_STEPS = 16;
7131
+ var TOTAL_STEPS = 26;
4449
7132
  var Generator = class {
4450
7133
  rootPath;
4451
7134
  config;
@@ -4453,7 +7136,7 @@ var Generator = class {
4453
7136
  this.config = config;
4454
7137
  this.rootPath = rootPath;
4455
7138
  }
4456
- /** Run the full 16-step generation pipeline */
7139
+ /** Run the full 26-step generation pipeline */
4457
7140
  async run() {
4458
7141
  logger.header(`Creating ${this.config.pascalCase} monorepo...`);
4459
7142
  logger.newline();
@@ -4466,9 +7149,19 @@ var Generator = class {
4466
7149
  this.writeLibPackage();
4467
7150
  this.writeUiPackage();
4468
7151
  this.writeDatabasePackage();
7152
+ this.writeCachePackage();
7153
+ this.writeEmailPackage();
7154
+ this.writeStoragePackage();
7155
+ this.writePaymentsPackage();
4469
7156
  this.writeNextjsApp();
7157
+ this.writeI18nLayer();
4470
7158
  this.writeBackendApp();
7159
+ this.writeApiDocs();
7160
+ this.writeLoggingLayer();
4471
7161
  this.writeAuthLayer();
7162
+ this.writeDockerFiles();
7163
+ this.writeStorybookConfig();
7164
+ this.writeE2eTests();
4472
7165
  this.writeSkills();
4473
7166
  await this.installDependencies();
4474
7167
  await this.initializeGit();
@@ -4497,8 +7190,7 @@ var Generator = class {
4497
7190
  logger.step(3, TOTAL_STEPS, "Creating directory structure...");
4498
7191
  const dirs = getExpectedDirectories(this.config);
4499
7192
  for (const dir of dirs) {
4500
- const fullPath = path.join(this.rootPath, dir);
4501
- fs.mkdirSync(fullPath, { recursive: true });
7193
+ fs.mkdirSync(path.join(this.rootPath, dir), { recursive: true });
4502
7194
  }
4503
7195
  logger.success(`Created ${dirs.length} directories`);
4504
7196
  }
@@ -4512,9 +7204,7 @@ var Generator = class {
4512
7204
  this.write("CONTRIBUTING.md", contributingMd(this.config));
4513
7205
  this.write("LICENSE", licenseText(this.config));
4514
7206
  const dockerContent = dockerCompose(this.config);
4515
- if (dockerContent) {
4516
- this.write("docker-compose.yml", dockerContent);
4517
- }
7207
+ if (dockerContent) this.write("docker-compose.yml", dockerContent);
4518
7208
  logger.success("Root files written");
4519
7209
  }
4520
7210
  // ── Step 5: Write GitHub files ───────────────────────────────────
@@ -4530,6 +7220,17 @@ var Generator = class {
4530
7220
  this.write(".github/ISSUE_TEMPLATE/feature_request.md", featureRequest());
4531
7221
  this.write(".github/pull_request_template.md", pullRequestTemplate());
4532
7222
  this.write(".github/workflows/ci.yml", ciWorkflow(this.config));
7223
+ if (this.config.hasE2e) {
7224
+ const e2eStrategy = createE2eStrategy(this.config.e2e);
7225
+ if (e2eStrategy) {
7226
+ const ciContent = fs.readFileSync(path.join(this.rootPath, ".github/workflows/ci.yml"), "utf-8");
7227
+ fs.writeFileSync(
7228
+ path.join(this.rootPath, ".github/workflows/ci.yml"),
7229
+ ciContent + e2eStrategy.ciWorkflow(this.config),
7230
+ "utf-8"
7231
+ );
7232
+ }
7233
+ }
4533
7234
  logger.success("GitHub files written");
4534
7235
  }
4535
7236
  // ── Step 6: Write config package ─────────────────────────────────
@@ -4598,9 +7299,114 @@ var Generator = class {
4598
7299
  }
4599
7300
  logger.success("Database package written");
4600
7301
  }
4601
- // ── Step 10: Write Next.js app ───────────────────────────────────
7302
+ // ── Step 10: Write cache package ─────────────────────────────────
7303
+ writeCachePackage() {
7304
+ if (!this.config.hasCache) {
7305
+ logger.step(10, TOTAL_STEPS, "Skipping cache package (--cache none)");
7306
+ return;
7307
+ }
7308
+ logger.step(10, TOTAL_STEPS, "Writing cache package (Redis)...");
7309
+ this.write("packages/cache/package.json", cachePackageJson(this.config));
7310
+ this.write("packages/cache/tsconfig.json", cacheTsConfig(this.config));
7311
+ this.write("packages/cache/src/index.ts", cacheIndex());
7312
+ this.write("packages/cache/src/client.ts", cacheClient());
7313
+ this.write("packages/cache/src/service.ts", cacheService());
7314
+ logger.success("Cache package written");
7315
+ }
7316
+ // ── Step 11: Write email package ─────────────────────────────────
7317
+ writeEmailPackage() {
7318
+ if (!this.config.hasEmail) {
7319
+ logger.step(11, TOTAL_STEPS, "Skipping email package (--email none)");
7320
+ return;
7321
+ }
7322
+ logger.step(11, TOTAL_STEPS, `Writing email package (${this.config.email})...`);
7323
+ const strategy = createEmailStrategy(this.config.email);
7324
+ if (!strategy) return;
7325
+ this.write("packages/email/package.json", strategy.packageJson(this.config));
7326
+ this.write("packages/email/tsconfig.json", `{
7327
+ "extends": "@${this.config.name}/config/typescript/base",
7328
+ "compilerOptions": {
7329
+ "jsx": "react-jsx",
7330
+ "outDir": "./dist",
7331
+ "rootDir": "./src",
7332
+ "verbatimModuleSyntax": false
7333
+ },
7334
+ "include": ["src/**/*"]
7335
+ }
7336
+ `);
7337
+ this.write("packages/email/src/index.ts", strategy.index(this.config));
7338
+ this.write("packages/email/src/client.ts", strategy.client(this.config));
7339
+ this.write("packages/email/src/send.ts", strategy.sendFunction(this.config));
7340
+ this.write("packages/email/src/templates/welcome.tsx", strategy.welcomeTemplate(this.config));
7341
+ this.write("packages/email/src/templates/reset-password.tsx", strategy.resetPasswordTemplate(this.config));
7342
+ logger.success("Email package written");
7343
+ }
7344
+ // ── Step 12: Write storage package ───────────────────────────────
7345
+ writeStoragePackage() {
7346
+ if (!this.config.hasStorage) {
7347
+ logger.step(12, TOTAL_STEPS, "Skipping storage package (--storage none)");
7348
+ return;
7349
+ }
7350
+ logger.step(12, TOTAL_STEPS, `Writing storage package (${this.config.storage})...`);
7351
+ const strategy = createStorageStrategy(this.config.storage);
7352
+ if (!strategy) return;
7353
+ this.write("packages/storage/package.json", strategy.packageJson(this.config));
7354
+ this.write("packages/storage/tsconfig.json", `{
7355
+ "extends": "@${this.config.name}/config/typescript/base",
7356
+ "compilerOptions": {
7357
+ "outDir": "./dist",
7358
+ "rootDir": "./src",
7359
+ "verbatimModuleSyntax": false
7360
+ },
7361
+ "include": ["src/**/*"]
7362
+ }
7363
+ `);
7364
+ this.write("packages/storage/src/index.ts", strategy.index(this.config));
7365
+ this.write("packages/storage/src/client.ts", strategy.client(this.config));
7366
+ this.write("packages/storage/src/upload.ts", strategy.uploadService(this.config));
7367
+ this.write("packages/storage/src/download.ts", strategy.downloadService(this.config));
7368
+ const apiRoutes = strategy.apiRoutes(this.config);
7369
+ for (const [routePath, content] of Object.entries(apiRoutes)) {
7370
+ this.write(routePath, content);
7371
+ }
7372
+ logger.success("Storage package written");
7373
+ }
7374
+ // ── Step 13: Write payments package ──────────────────────────────
7375
+ writePaymentsPackage() {
7376
+ if (!this.config.hasPayments) {
7377
+ logger.step(13, TOTAL_STEPS, "Skipping payments package (--payments none)");
7378
+ return;
7379
+ }
7380
+ logger.step(13, TOTAL_STEPS, `Writing payments package (${this.config.payments})...`);
7381
+ const strategy = createPaymentStrategy(this.config.payments);
7382
+ if (!strategy) return;
7383
+ this.write("packages/payments/package.json", strategy.packageJson(this.config));
7384
+ this.write("packages/payments/tsconfig.json", `{
7385
+ "extends": "@${this.config.name}/config/typescript/base",
7386
+ "compilerOptions": {
7387
+ "outDir": "./dist",
7388
+ "rootDir": "./src",
7389
+ "verbatimModuleSyntax": false
7390
+ },
7391
+ "include": ["src/**/*"]
7392
+ }
7393
+ `);
7394
+ this.write("packages/payments/src/index.ts", strategy.index(this.config));
7395
+ this.write("packages/payments/src/client.ts", strategy.client(this.config));
7396
+ this.write("packages/payments/src/webhook.ts", strategy.webhookHandler(this.config));
7397
+ this.write("packages/payments/src/checkout.ts", strategy.checkout(this.config));
7398
+ this.write("packages/payments/src/subscription.ts", strategy.subscription(this.config));
7399
+ if (this.config.backend === "nestjs") {
7400
+ this.write("apps/api/src/payments/payments.controller.ts", strategy.apiRoute(this.config));
7401
+ } else {
7402
+ this.write("apps/api/src/routes/payments.ts", strategy.apiRoute(this.config));
7403
+ }
7404
+ this.write("apps/web/app/pricing/page.tsx", strategy.pricingPage(this.config));
7405
+ logger.success("Payments package written");
7406
+ }
7407
+ // ── Step 14: Write Next.js app ───────────────────────────────────
4602
7408
  writeNextjsApp() {
4603
- logger.step(10, TOTAL_STEPS, "Writing Next.js 15 app...");
7409
+ logger.step(14, TOTAL_STEPS, "Writing Next.js 15 app...");
4604
7410
  this.write("apps/web/package.json", webPackageJson(this.config));
4605
7411
  this.write("apps/web/tsconfig.json", webTsConfig(this.config));
4606
7412
  this.write("apps/web/next.config.ts", nextConfig(this.config));
@@ -4628,15 +7434,30 @@ var Generator = class {
4628
7434
  this.write(`apps/web/${stateDir}/theme-slice.ts`, stateStrategy.exampleStore(this.config));
4629
7435
  }
4630
7436
  const provider = stateStrategy.providerWrapper(this.config);
4631
- if (provider) {
4632
- this.write(`apps/web/${stateDir}/provider.tsx`, provider);
4633
- }
7437
+ if (provider) this.write(`apps/web/${stateDir}/provider.tsx`, provider);
4634
7438
  }
4635
7439
  logger.success("Next.js app written");
4636
7440
  }
4637
- // ── Step 11: Write backend app ───────────────────────────────────
7441
+ // ── Step 15: Write i18n layer ────────────────────────────────────
7442
+ writeI18nLayer() {
7443
+ if (!this.config.hasI18n) {
7444
+ logger.step(15, TOTAL_STEPS, "Skipping i18n (--i18n none)");
7445
+ return;
7446
+ }
7447
+ logger.step(15, TOTAL_STEPS, "Writing i18n layer (next-intl)...");
7448
+ this.write("apps/web/i18n/request.ts", i18nRequest());
7449
+ this.write("apps/web/i18n/routing.ts", i18nRouting());
7450
+ this.write("apps/web/i18n/navigation.ts", i18nNavigation());
7451
+ this.write("apps/web/messages/en.json", messagesEn(this.config));
7452
+ this.write("apps/web/messages/ar.json", messagesAr(this.config));
7453
+ this.write("apps/web/app/[locale]/layout.tsx", i18nLayout(this.config));
7454
+ this.write("apps/web/app/[locale]/page.tsx", i18nPage(this.config));
7455
+ this.write("apps/web/components/language-switcher.tsx", languageSwitcher());
7456
+ logger.success("i18n layer written");
7457
+ }
7458
+ // ── Step 16: Write backend app ───────────────────────────────────
4638
7459
  writeBackendApp() {
4639
- logger.step(11, TOTAL_STEPS, `Writing ${this.config.backend} backend...`);
7460
+ logger.step(16, TOTAL_STEPS, `Writing ${this.config.backend} backend...`);
4640
7461
  const backendStrategy = createBackendStrategy(this.config.backend);
4641
7462
  this.write("apps/api/package.json", backendStrategy.packageJson(this.config));
4642
7463
  this.write("apps/api/tsconfig.json", backendStrategy.tsConfig(this.config));
@@ -4661,13 +7482,39 @@ var Generator = class {
4661
7482
  }
4662
7483
  logger.success(`${this.config.backend === "nestjs" ? "NestJS" : "Express"} backend written`);
4663
7484
  }
4664
- // ── Step 12: Write auth layer ────────────────────────────────────
7485
+ // ── Step 17: Write API docs ──────────────────────────────────────
7486
+ writeApiDocs() {
7487
+ if (!this.config.hasApiDocs) {
7488
+ logger.step(17, TOTAL_STEPS, "Skipping API docs (--api-docs none)");
7489
+ return;
7490
+ }
7491
+ logger.step(17, TOTAL_STEPS, `Writing API docs (${this.config.apiDocs})...`);
7492
+ const strategy = createApiDocsStrategy(this.config.apiDocs);
7493
+ if (!strategy) return;
7494
+ this.write("apps/api/src/docs/swagger-config.ts", strategy.docsConfig(this.config));
7495
+ this.write("apps/api/src/docs/setup.ts", strategy.docsSetup(this.config));
7496
+ logger.success("API docs written");
7497
+ }
7498
+ // ── Step 18: Write logging layer ─────────────────────────────────
7499
+ writeLoggingLayer() {
7500
+ if (!this.config.hasLogging) {
7501
+ logger.step(18, TOTAL_STEPS, "Skipping logging (--logging default)");
7502
+ return;
7503
+ }
7504
+ logger.step(18, TOTAL_STEPS, `Writing logging layer (${this.config.logging})...`);
7505
+ const strategy = createLoggingStrategy(this.config.logging);
7506
+ if (!strategy) return;
7507
+ this.write("packages/lib/src/logger/index.ts", strategy.index(this.config));
7508
+ this.write("packages/lib/src/logger/logger.ts", strategy.loggerSetup(this.config));
7509
+ logger.success("Logging layer written");
7510
+ }
7511
+ // ── Step 19: Write auth layer ────────────────────────────────────
4665
7512
  writeAuthLayer() {
4666
7513
  if (!this.config.hasAuth) {
4667
- logger.step(12, TOTAL_STEPS, "Skipping auth layer (--auth none)");
7514
+ logger.step(19, TOTAL_STEPS, "Skipping auth layer (--auth none)");
4668
7515
  return;
4669
7516
  }
4670
- logger.step(12, TOTAL_STEPS, `Writing ${this.config.auth} auth layer...`);
7517
+ logger.step(19, TOTAL_STEPS, `Writing ${this.config.auth} auth layer...`);
4671
7518
  const authStrategy = createAuthStrategy(this.config.auth);
4672
7519
  if (!authStrategy) return;
4673
7520
  if (this.config.auth === "next-auth") {
@@ -4683,9 +7530,60 @@ var Generator = class {
4683
7530
  }
4684
7531
  logger.success("Auth layer written");
4685
7532
  }
4686
- // ── Step 13: Write AI skills ─────────────────────────────────────
7533
+ // ── Step 20: Write Docker files ──────────────────────────────────
7534
+ writeDockerFiles() {
7535
+ if (!this.config.hasDocker) {
7536
+ logger.step(20, TOTAL_STEPS, "Skipping Docker files (--docker none)");
7537
+ return;
7538
+ }
7539
+ logger.step(20, TOTAL_STEPS, `Writing Docker files (${this.config.docker})...`);
7540
+ const strategy = createDockerStrategy(this.config.docker);
7541
+ if (!strategy) return;
7542
+ this.write("apps/web/Dockerfile", strategy.webDockerfile(this.config));
7543
+ this.write("apps/api/Dockerfile", strategy.apiDockerfile(this.config));
7544
+ this.write("docker-compose.prod.yml", strategy.dockerComposeProd(this.config));
7545
+ this.write(".dockerignore", strategy.dockerignore(this.config));
7546
+ const extraFiles = strategy.extraFiles(this.config);
7547
+ for (const [filePath, content] of Object.entries(extraFiles)) {
7548
+ this.write(filePath, content);
7549
+ }
7550
+ logger.success("Docker files written");
7551
+ }
7552
+ // ── Step 21: Write Storybook config ──────────────────────────────
7553
+ writeStorybookConfig() {
7554
+ if (!this.config.storybook) {
7555
+ logger.step(21, TOTAL_STEPS, "Skipping Storybook (--no-storybook)");
7556
+ return;
7557
+ }
7558
+ logger.step(21, TOTAL_STEPS, "Writing Storybook config...");
7559
+ this.write("packages/ui/.storybook/main.ts", storybookMain(this.config));
7560
+ this.write("packages/ui/.storybook/preview.ts", storybookPreview(this.config));
7561
+ this.write("packages/ui/src/components/button.stories.tsx", buttonStories(this.config));
7562
+ this.write("packages/ui/src/components/card.stories.tsx", cardStories(this.config));
7563
+ this.write("packages/ui/src/components/input.stories.tsx", inputStories(this.config));
7564
+ logger.success("Storybook config written");
7565
+ }
7566
+ // ── Step 22: Write E2E tests ─────────────────────────────────────
7567
+ writeE2eTests() {
7568
+ if (!this.config.hasE2e) {
7569
+ logger.step(22, TOTAL_STEPS, "Skipping E2E tests (--e2e none)");
7570
+ return;
7571
+ }
7572
+ logger.step(22, TOTAL_STEPS, `Writing E2E tests (${this.config.e2e})...`);
7573
+ const strategy = createE2eStrategy(this.config.e2e);
7574
+ if (!strategy) return;
7575
+ if (this.config.e2e === "playwright") {
7576
+ this.write("apps/web/playwright.config.ts", strategy.config(this.config));
7577
+ this.write("apps/web/e2e/example.spec.ts", strategy.exampleTest(this.config));
7578
+ } else {
7579
+ this.write("apps/web/cypress.config.ts", strategy.config(this.config));
7580
+ this.write("apps/web/cypress/e2e/example.cy.ts", strategy.exampleTest(this.config));
7581
+ }
7582
+ logger.success("E2E tests written");
7583
+ }
7584
+ // ── Step 23: Write AI skills ─────────────────────────────────────
4687
7585
  writeSkills() {
4688
- logger.step(13, TOTAL_STEPS, "Writing AI agent skills...");
7586
+ logger.step(23, TOTAL_STEPS, "Writing AI agent skills...");
4689
7587
  this.write(".claude/settings.json", claudeSettings(this.config));
4690
7588
  this.write(".claude/skills/component-design/SKILL.md", componentDesignSkill(this.config));
4691
7589
  this.write(".claude/skills/page-design/SKILL.md", pageDesignSkill(this.config));
@@ -4693,42 +7591,35 @@ var Generator = class {
4693
7591
  this.write(".claude/skills/monorepo-doctor/SKILL.md", monrepoDoctorSkill());
4694
7592
  logger.success("AI skills written");
4695
7593
  }
4696
- // ── Step 14: Install dependencies ────────────────────────────────
7594
+ // ── Step 24: Install dependencies ────────────────────────────────
4697
7595
  async installDependencies() {
4698
- logger.step(14, TOTAL_STEPS, `Installing dependencies (${this.config.packageManager})...`);
7596
+ logger.step(24, TOTAL_STEPS, `Installing dependencies (${this.config.packageManager})...`);
4699
7597
  try {
4700
- execSync(this.config.installCommand, {
4701
- cwd: this.rootPath,
4702
- stdio: "pipe",
4703
- timeout: 12e4
4704
- });
7598
+ execSync(this.config.installCommand, { cwd: this.rootPath, stdio: "pipe", timeout: 12e4 });
4705
7599
  logger.success("Dependencies installed");
4706
7600
  } catch {
4707
7601
  logger.warn("Dependency installation failed \u2014 run manually after setup");
4708
7602
  }
4709
7603
  }
4710
- // ── Step 15: Initialize git ──────────────────────────────────────
7604
+ // ── Step 25: Initialize git ──────────────────────────────────────
4711
7605
  async initializeGit() {
4712
7606
  if (!this.config.gitInit) {
4713
- logger.step(15, TOTAL_STEPS, "Skipping git init (--no-git)");
7607
+ logger.step(25, TOTAL_STEPS, "Skipping git init (--no-git)");
4714
7608
  return;
4715
7609
  }
4716
- logger.step(15, TOTAL_STEPS, "Initializing git repository...");
7610
+ logger.step(25, TOTAL_STEPS, "Initializing git repository...");
4717
7611
  try {
4718
7612
  execSync("git init", { cwd: this.rootPath, stdio: "pipe" });
4719
7613
  execSync("git add .", { cwd: this.rootPath, stdio: "pipe" });
4720
- execSync(`git commit -m "Initial commit: ${this.config.pascalCase} monorepo"`, {
4721
- cwd: this.rootPath,
4722
- stdio: "pipe"
4723
- });
7614
+ execSync(`git commit -m "Initial commit: ${this.config.pascalCase} monorepo"`, { cwd: this.rootPath, stdio: "pipe" });
4724
7615
  logger.success("Git repository initialized with initial commit");
4725
7616
  } catch {
4726
7617
  logger.warn("Git initialization failed \u2014 run manually");
4727
7618
  }
4728
7619
  }
4729
- // ── Step 16: Print summary ───────────────────────────────────────
7620
+ // ── Step 26: Print summary ───────────────────────────────────────
4730
7621
  printSummary() {
4731
- logger.step(16, TOTAL_STEPS, "Done!");
7622
+ logger.step(26, TOTAL_STEPS, "Done!");
4732
7623
  logger.summary([
4733
7624
  `${this.config.pascalCase} monorepo created successfully!`,
4734
7625
  "",
@@ -4743,8 +7634,7 @@ var Generator = class {
4743
7634
  write(relativePath, content) {
4744
7635
  if (!content) return;
4745
7636
  const fullPath = path.join(this.rootPath, relativePath);
4746
- const dir = path.dirname(fullPath);
4747
- fs.mkdirSync(dir, { recursive: true });
7637
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
4748
7638
  fs.writeFileSync(fullPath, content, "utf-8");
4749
7639
  }
4750
7640
  };
@@ -4783,7 +7673,17 @@ function detectProjectConfig(rootPath) {
4783
7673
  packageManager,
4784
7674
  gitInit: false,
4785
7675
  githubFiles,
4786
- license
7676
+ license,
7677
+ docker: detectDocker(rootPath),
7678
+ i18n: detectI18n(rootPath),
7679
+ payments: detectPayments(rootPath),
7680
+ email: detectEmail(rootPath),
7681
+ apiDocs: detectApiDocsOption(rootPath),
7682
+ storage: detectStorage(rootPath),
7683
+ e2e: detectE2e(rootPath),
7684
+ storybook: fs2.existsSync(path2.join(rootPath, "packages/ui/.storybook")),
7685
+ cache: detectCache(rootPath),
7686
+ logging: detectLogging(rootPath)
4787
7687
  });
4788
7688
  }
4789
7689
  function detectBackend(rootPath, _name) {
@@ -4904,6 +7804,72 @@ function detectLicense(rootPath) {
4904
7804
  if (content.includes("proprietary") || content.includes("All rights reserved")) return "proprietary";
4905
7805
  return "MIT";
4906
7806
  }
7807
+ function detectDocker(rootPath) {
7808
+ if (fs2.existsSync(path2.join(rootPath, "nginx/nginx.conf"))) return "full";
7809
+ if (fs2.existsSync(path2.join(rootPath, "apps/web/Dockerfile"))) return "minimal";
7810
+ return "none";
7811
+ }
7812
+ function detectI18n(rootPath) {
7813
+ const webPkg = readJson(path2.join(rootPath, "apps/web/package.json"));
7814
+ const deps = { ...webPkg?.dependencies, ...webPkg?.devDependencies };
7815
+ if (deps["next-intl"]) return "next-intl";
7816
+ return "none";
7817
+ }
7818
+ function detectPayments(rootPath) {
7819
+ const pkg = readJson(path2.join(rootPath, "packages/payments/package.json"));
7820
+ if (!pkg) return "none";
7821
+ const deps = pkg.dependencies;
7822
+ if (deps?.["stripe"]) return "stripe";
7823
+ if (deps?.["@lemonsqueezy/lemonsqueezy.js"]) return "lemonsqueezy";
7824
+ if (deps?.["@paddle/paddle-node-sdk"]) return "paddle";
7825
+ return "none";
7826
+ }
7827
+ function detectEmail(rootPath) {
7828
+ const pkg = readJson(path2.join(rootPath, "packages/email/package.json"));
7829
+ if (!pkg) return "none";
7830
+ const deps = pkg.dependencies;
7831
+ if (deps?.["resend"]) return "resend";
7832
+ if (deps?.["nodemailer"]) return "nodemailer";
7833
+ if (deps?.["@sendgrid/mail"]) return "sendgrid";
7834
+ return "none";
7835
+ }
7836
+ function detectApiDocsOption(rootPath) {
7837
+ if (fs2.existsSync(path2.join(rootPath, "apps/api/src/docs/setup.ts"))) {
7838
+ const content = readFile(path2.join(rootPath, "apps/api/src/docs/setup.ts"));
7839
+ if (content?.includes("redoc")) return "redoc";
7840
+ return "swagger";
7841
+ }
7842
+ return "none";
7843
+ }
7844
+ function detectStorage(rootPath) {
7845
+ const pkg = readJson(path2.join(rootPath, "packages/storage/package.json"));
7846
+ if (!pkg) return "none";
7847
+ const deps = pkg.dependencies;
7848
+ if (deps?.["@aws-sdk/client-s3"]) return "s3";
7849
+ if (deps?.["uploadthing"]) return "uploadthing";
7850
+ if (deps?.["cloudinary"]) return "cloudinary";
7851
+ return "none";
7852
+ }
7853
+ function detectE2e(rootPath) {
7854
+ if (fs2.existsSync(path2.join(rootPath, "apps/web/playwright.config.ts"))) return "playwright";
7855
+ if (fs2.existsSync(path2.join(rootPath, "apps/web/cypress.config.ts"))) return "cypress";
7856
+ return "none";
7857
+ }
7858
+ function detectCache(rootPath) {
7859
+ const pkg = readJson(path2.join(rootPath, "packages/cache/package.json"));
7860
+ if (!pkg) return "none";
7861
+ const deps = pkg.dependencies;
7862
+ if (deps?.["ioredis"]) return "redis";
7863
+ return "none";
7864
+ }
7865
+ function detectLogging(rootPath) {
7866
+ if (fs2.existsSync(path2.join(rootPath, "packages/lib/src/logger/logger.ts"))) {
7867
+ const content = readFile(path2.join(rootPath, "packages/lib/src/logger/logger.ts"));
7868
+ if (content?.includes("pino")) return "pino";
7869
+ if (content?.includes("winston")) return "winston";
7870
+ }
7871
+ return "default";
7872
+ }
4907
7873
  function readJson(filePath) {
4908
7874
  try {
4909
7875
  const content = fs2.readFileSync(filePath, "utf-8");
@@ -5496,10 +8462,10 @@ var Workflow = class {
5496
8462
  };
5497
8463
 
5498
8464
  // src/cli.ts
5499
- var VERSION = "1.0.0";
8465
+ var VERSION = "2.0.0";
5500
8466
  var program = new Command();
5501
8467
  program.name("create-next-monorepo").description("Generate production-ready Next.js + NestJS/Express monorepos with Turborepo").version(VERSION);
5502
- program.argument("[name]", "Project name (kebab-case, e.g. my-app)").option("-b, --backend <framework>", "Backend framework: nestjs, express", "nestjs").option("-s, --styling <approach>", "Styling: tailwind, css-modules, styled-components", "tailwind").option("--orm <orm>", "Database ORM: prisma, drizzle, none", "prisma").option("--db <database>", "Database: postgres, mysql, sqlite, mongodb", "postgres").option("--auth <auth>", "Auth: next-auth, custom, none", "next-auth").option("--state <state>", "State: zustand, jotai, redux, tanstack-query, none", "zustand").option("--testing <framework>", "Testing: vitest, jest", "vitest").option("--license <license>", "License type", "MIT").option("--git", "Initialize git repository (default: true)", true).option("--no-git", "Skip git initialization").option("--github", "Generate GitHub community files", false).option("--package-manager <pm>", "Package manager: pnpm, npm, yarn, bun", "pnpm").action(async (name, options) => {
8468
+ program.argument("[name]", "Project name (kebab-case, e.g. my-app)").option("-b, --backend <framework>", "Backend framework: nestjs, express", "nestjs").option("-s, --styling <approach>", "Styling: tailwind, css-modules, styled-components", "tailwind").option("--orm <orm>", "Database ORM: prisma, drizzle, none", "prisma").option("--db <database>", "Database: postgres, mysql, sqlite, mongodb", "postgres").option("--auth <auth>", "Auth: next-auth, custom, none", "next-auth").option("--state <state>", "State: zustand, jotai, redux, tanstack-query, none", "zustand").option("--testing <framework>", "Testing: vitest, jest", "vitest").option("--license <license>", "License type", "MIT").option("--git", "Initialize git repository (default: true)", true).option("--no-git", "Skip git initialization").option("--github", "Generate GitHub community files", false).option("--package-manager <pm>", "Package manager: pnpm, npm, yarn, bun", "pnpm").option("--docker <mode>", "Docker: full, minimal, none", "none").option("--i18n <lib>", "i18n: next-intl, none", "none").option("--payments <provider>", "Payments: stripe, lemonsqueezy, paddle, none", "none").option("--email <provider>", "Email: resend, nodemailer, sendgrid, none", "none").option("--api-docs <renderer>", "API docs: swagger, redoc, none", "none").option("--storage <provider>", "Storage: s3, uploadthing, cloudinary, none", "none").option("--e2e <framework>", "E2E testing: playwright, cypress, none", "none").option("--storybook", "Generate Storybook for UI package", false).option("--cache <provider>", "Cache: redis, none", "none").option("--logging <lib>", "Logging: pino, winston, default", "default").action(async (name, options) => {
5503
8469
  if (!name) {
5504
8470
  program.help();
5505
8471
  return;
@@ -5524,7 +8490,16 @@ program.argument("[name]", "Project name (kebab-case, e.g. my-app)").option("-b,
5524
8490
  ["auth", options.auth, validAuths],
5525
8491
  ["state", options.state, validStates],
5526
8492
  ["testing", options.testing, validTesting],
5527
- ["package-manager", options.packageManager, validPms]
8493
+ ["package-manager", options.packageManager, validPms],
8494
+ ["docker", options.docker, ["full", "minimal", "none"]],
8495
+ ["i18n", options.i18n, ["next-intl", "none"]],
8496
+ ["payments", options.payments, ["stripe", "lemonsqueezy", "paddle", "none"]],
8497
+ ["email", options.email, ["resend", "nodemailer", "sendgrid", "none"]],
8498
+ ["api-docs", options.apiDocs, ["swagger", "redoc", "none"]],
8499
+ ["storage", options.storage, ["s3", "uploadthing", "cloudinary", "none"]],
8500
+ ["e2e", options.e2e, ["playwright", "cypress", "none"]],
8501
+ ["cache", options.cache, ["redis", "none"]],
8502
+ ["logging", options.logging, ["pino", "winston", "default"]]
5528
8503
  ];
5529
8504
  for (const [flag, value, allowed] of validations) {
5530
8505
  if (!allowed.includes(value)) {
@@ -5545,7 +8520,17 @@ program.argument("[name]", "Project name (kebab-case, e.g. my-app)").option("-b,
5545
8520
  license: options.license,
5546
8521
  packageManager: options.packageManager,
5547
8522
  gitInit: options.git,
5548
- githubFiles: options.github
8523
+ githubFiles: options.github,
8524
+ docker: options.docker,
8525
+ i18n: options.i18n,
8526
+ payments: options.payments,
8527
+ email: options.email,
8528
+ apiDocs: options.apiDocs,
8529
+ storage: options.storage,
8530
+ e2e: options.e2e,
8531
+ storybook: options.storybook,
8532
+ cache: options.cache,
8533
+ logging: options.logging
5549
8534
  });
5550
8535
  const rootPath = path4.resolve(process.cwd(), name);
5551
8536
  const generator = new Generator(config, rootPath);