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