plazercli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,4059 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/args.ts
4
+ import { Command } from "commander";
5
+ import { readFileSync } from "fs";
6
+ import { fileURLToPath } from "url";
7
+ import path from "path";
8
+ var __dirname = path.dirname(fileURLToPath(import.meta.url));
9
+ var pkgPath = path.resolve(__dirname, "../package.json");
10
+ var pkgPathAlt = path.resolve(__dirname, "../../package.json");
11
+ var pkg = JSON.parse(
12
+ (() => {
13
+ try {
14
+ return readFileSync(pkgPath, "utf-8");
15
+ } catch {
16
+ return readFileSync(pkgPathAlt, "utf-8");
17
+ }
18
+ })()
19
+ );
20
+ function parseArgs() {
21
+ const program = new Command();
22
+ program.name("plazercli").description("CLI para gerar projetos fullstack boilerplate").version(pkg.version, "-v, --version").option("-n, --name <name>", "Nome do projeto").option("-y, --yes", "Usar valores padr\xE3o (skip prompts)").parse();
23
+ const opts = program.opts();
24
+ return {
25
+ name: opts.name,
26
+ yes: opts.yes
27
+ };
28
+ }
29
+
30
+ // src/cli/prompts.ts
31
+ import { input, select, confirm, checkbox } from "@inquirer/prompts";
32
+ import path2 from "path";
33
+ async function runPrompts(flags) {
34
+ const projectName = flags.name ?? await input({
35
+ message: "\u{1F4E6} Qual o nome do projeto?",
36
+ validate: (value) => {
37
+ if (!value.trim()) return "O nome do projeto \xE9 obrigat\xF3rio";
38
+ if (!/^[a-z0-9-]+$/.test(value)) return "Use apenas letras min\xFAsculas, n\xFAmeros e h\xEDfens";
39
+ return true;
40
+ }
41
+ });
42
+ const projectDescription = await input({
43
+ message: "\u{1F4DD} Descreva brevemente o projeto:",
44
+ default: `Projeto ${projectName}`
45
+ });
46
+ const runtime = await select({
47
+ message: "\u26A1 Qual runtime/gerenciador de pacotes usar?",
48
+ choices: [
49
+ { name: "pnpm (Recomendado)", value: "pnpm" },
50
+ { name: "bun", value: "bun" },
51
+ { name: "npm", value: "npm" }
52
+ ]
53
+ });
54
+ const database = await select({
55
+ message: "\u{1F5C4}\uFE0F Qual banco de dados usar?",
56
+ choices: [
57
+ { name: "PostgreSQL", value: "postgresql" },
58
+ { name: "MySQL", value: "mysql" },
59
+ { name: "MongoDB", value: "mongodb" },
60
+ { name: "Nenhum", value: "none" }
61
+ ]
62
+ });
63
+ const multiTenant = await confirm({
64
+ message: "\u{1F3E2} Usar multi-tenant?",
65
+ default: false
66
+ });
67
+ let smtp = await confirm({
68
+ message: "\u{1F4E7} Configurar servidor SMTP (envio de emails)?",
69
+ default: false
70
+ });
71
+ const authChoices = await checkbox({
72
+ message: "\u{1F510} Quais m\xE9todos de autentica\xE7\xE3o usar?",
73
+ choices: [
74
+ { name: "JWT (JSON Web Token)", value: "jwt" },
75
+ { name: "Magic Link (login por email)", value: "magicLink" },
76
+ { name: "Google OAuth", value: "googleOAuth" }
77
+ ]
78
+ });
79
+ const auth = {
80
+ jwt: authChoices.includes("jwt"),
81
+ magicLink: authChoices.includes("magicLink"),
82
+ googleOAuth: authChoices.includes("googleOAuth")
83
+ };
84
+ if (auth.magicLink && !smtp) {
85
+ smtp = true;
86
+ console.log(" \u2139 SMTP habilitado automaticamente (necess\xE1rio para Magic Link)");
87
+ }
88
+ let redis = await confirm({
89
+ message: "\u{1F534} Usar Redis (cache/sess\xF5es)?",
90
+ default: false
91
+ });
92
+ const queue = await select({
93
+ message: "\u{1F4EC} Usar filas e jobs?",
94
+ choices: [
95
+ { name: "BullMQ (requer Redis)", value: "bullmq" },
96
+ { name: "RabbitMQ", value: "rabbitmq" },
97
+ { name: "Nenhum", value: "none" }
98
+ ]
99
+ });
100
+ if (queue === "bullmq" && !redis) {
101
+ redis = true;
102
+ console.log(" \u2139 Redis habilitado automaticamente (necess\xE1rio para BullMQ)");
103
+ }
104
+ const frontend = await select({
105
+ message: "\u{1F3A8} Qual framework de frontend?",
106
+ choices: [
107
+ { name: "Next.js", value: "nextjs" },
108
+ { name: "React (Vite)", value: "react-vite" },
109
+ { name: "Vue", value: "vue" },
110
+ { name: "Angular", value: "angular" }
111
+ ]
112
+ });
113
+ const backend = await select({
114
+ message: "\u{1F527} Qual framework de backend?",
115
+ choices: [
116
+ { name: "Express", value: "express" },
117
+ { name: "Fastify", value: "fastify" },
118
+ { name: "NestJS", value: "nestjs" }
119
+ ]
120
+ });
121
+ const integrationChoices = await checkbox({
122
+ message: "\u{1F50C} Quais integra\xE7\xF5es usar?",
123
+ choices: [
124
+ { name: "ViaCEP (consulta CEP)", value: "viacep" },
125
+ { name: "WhatsApp", value: "whatsapp" },
126
+ { name: "Stripe (pagamentos)", value: "stripe" },
127
+ { name: "Mercado Pago (pagamentos)", value: "mercadoPago" },
128
+ { name: "AbacatePay (pagamentos)", value: "abacatePay" }
129
+ ]
130
+ });
131
+ const integrations = {
132
+ viacep: integrationChoices.includes("viacep"),
133
+ whatsapp: integrationChoices.includes("whatsapp"),
134
+ stripe: integrationChoices.includes("stripe"),
135
+ mercadoPago: integrationChoices.includes("mercadoPago"),
136
+ abacatePay: integrationChoices.includes("abacatePay")
137
+ };
138
+ const minio = await confirm({
139
+ message: "\u{1F4E6} Usar MinIO para storage (S3-compatible)?",
140
+ default: false
141
+ });
142
+ const pm2 = await confirm({
143
+ message: "\u{1F504} Usar PM2 para atualiza\xE7\xF5es sem downtime?",
144
+ default: false
145
+ });
146
+ const projectDir = path2.resolve(process.cwd(), projectName);
147
+ const usePrisma = database === "postgresql" || database === "mysql";
148
+ const useMongoose = database === "mongodb";
149
+ const hasAnyAuth = auth.jwt || auth.magicLink || auth.googleOAuth;
150
+ const hasAnyIntegration = Object.values(integrations).some(Boolean);
151
+ const hasAnyQueue = queue !== "none";
152
+ return {
153
+ projectName,
154
+ projectDescription,
155
+ runtime,
156
+ database,
157
+ multiTenant,
158
+ smtp,
159
+ redis,
160
+ minio,
161
+ pm2,
162
+ auth,
163
+ queue,
164
+ frontend,
165
+ backend,
166
+ integrations,
167
+ projectDir,
168
+ usePrisma,
169
+ useMongoose,
170
+ hasAnyAuth,
171
+ hasAnyIntegration,
172
+ hasAnyQueue
173
+ };
174
+ }
175
+
176
+ // src/helpers/logger.ts
177
+ import chalk from "chalk";
178
+ var logger = {
179
+ info: (msg) => console.log(chalk.cyan(" \u2139"), msg),
180
+ success: (msg) => console.log(chalk.green(" \u2714"), msg),
181
+ warn: (msg) => console.log(chalk.yellow(" \u26A0"), msg),
182
+ error: (msg) => console.log(chalk.red(" \u2716"), msg),
183
+ step: (msg) => console.log(chalk.blue(" \u2192"), msg),
184
+ title: (msg) => console.log(chalk.bold.white(`
185
+ ${msg}
186
+ `)),
187
+ blank: () => console.log()
188
+ };
189
+ function printBanner() {
190
+ console.log(
191
+ chalk.bold.cyan(`
192
+ \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
193
+ \u2551 \u2551
194
+ \u2551 \u{1F680} PlazerCLI v1.0 \u2551
195
+ \u2551 Fullstack Boilerplate Generator \u2551
196
+ \u2551 \u2551
197
+ \u2551 Oi! Eu sou a Liz, sua assistente. \u2551
198
+ \u2551 Vou configurar seu projeto inteiro. \u2551
199
+ \u2551 \u2551
200
+ \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
201
+ `)
202
+ );
203
+ }
204
+ function printSummary(config) {
205
+ const authMethods = [
206
+ config.auth.jwt && "JWT",
207
+ config.auth.magicLink && "Magic Link",
208
+ config.auth.googleOAuth && "Google OAuth"
209
+ ].filter(Boolean);
210
+ const integrations = Object.entries(config.integrations).filter(([, v]) => v).map(([k]) => k);
211
+ const infra = [
212
+ config.redis && "Redis",
213
+ config.smtp && "SMTP",
214
+ config.minio && "MinIO",
215
+ config.pm2 && "PM2",
216
+ config.multiTenant && "Multi-tenant"
217
+ ].filter(Boolean);
218
+ console.log(chalk.bold.white("\n \u{1F4CB} Liz preparou o resumo do projeto:\n"));
219
+ console.log(chalk.white(` Projeto: ${chalk.cyan(config.projectName)}`));
220
+ console.log(chalk.white(` Frontend: ${chalk.cyan(config.frontend)}`));
221
+ console.log(chalk.white(` Backend: ${chalk.cyan(config.backend)}`));
222
+ console.log(chalk.white(` Banco de dados: ${chalk.cyan(config.database)}`));
223
+ if (authMethods.length > 0) {
224
+ console.log(chalk.white(` Autentica\xE7\xE3o: ${chalk.cyan(authMethods.join(", "))}`));
225
+ }
226
+ if (config.queue !== "none") {
227
+ console.log(chalk.white(` Filas: ${chalk.cyan(config.queue)}`));
228
+ }
229
+ if (infra.length > 0) {
230
+ console.log(chalk.white(` Infra: ${chalk.cyan(infra.join(", "))}`));
231
+ }
232
+ if (integrations.length > 0) {
233
+ console.log(chalk.white(` Integra\xE7\xF5es: ${chalk.cyan(integrations.join(", "))}`));
234
+ }
235
+ console.log();
236
+ }
237
+
238
+ // src/generators/project.ts
239
+ import path30 from "path";
240
+ import chalk2 from "chalk";
241
+
242
+ // src/generators/base.ts
243
+ import path4 from "path";
244
+
245
+ // src/helpers/filesystem.ts
246
+ import fs from "fs-extra";
247
+ import path3 from "path";
248
+ import { fileURLToPath as fileURLToPath2 } from "url";
249
+ var __dirname2 = path3.dirname(fileURLToPath2(import.meta.url));
250
+ var TEMPLATE_DIR = fs.existsSync(path3.resolve(__dirname2, "../../templates")) ? path3.resolve(__dirname2, "../../templates") : path3.resolve(__dirname2, "../templates");
251
+ function getTemplatePath(...segments) {
252
+ return path3.join(TEMPLATE_DIR, ...segments);
253
+ }
254
+ async function ensureDir(dir) {
255
+ await fs.ensureDir(dir);
256
+ }
257
+ async function copyFile(src, dest) {
258
+ await fs.ensureDir(path3.dirname(dest));
259
+ await fs.copyFile(src, dest);
260
+ }
261
+ async function writeFile(filePath, content) {
262
+ await fs.ensureDir(path3.dirname(filePath));
263
+ await fs.writeFile(filePath, content, "utf-8");
264
+ }
265
+ async function readFile(filePath) {
266
+ return fs.readFile(filePath, "utf-8");
267
+ }
268
+
269
+ // src/helpers/template.ts
270
+ import ejs from "ejs";
271
+ async function renderTemplate(templatePath, data) {
272
+ const template = await readFile(templatePath);
273
+ return ejs.render(template, data, { async: false });
274
+ }
275
+
276
+ // src/helpers/runtime.ts
277
+ import { execa } from "execa";
278
+ function getInstallCommand(runtime) {
279
+ switch (runtime) {
280
+ case "bun":
281
+ return ["bun", ["install"]];
282
+ case "pnpm":
283
+ return ["pnpm", ["install"]];
284
+ case "npm":
285
+ return ["npm", ["install"]];
286
+ }
287
+ }
288
+ function getRunCommand(runtime) {
289
+ switch (runtime) {
290
+ case "bun":
291
+ return "bun run";
292
+ case "pnpm":
293
+ return "pnpm run";
294
+ case "npm":
295
+ return "npm run";
296
+ }
297
+ }
298
+ async function isCommandAvailable(cmd) {
299
+ try {
300
+ await execa("which", [cmd]);
301
+ return true;
302
+ } catch {
303
+ return false;
304
+ }
305
+ }
306
+ async function installDependencies(projectDir, runtime) {
307
+ const [cmd, args] = getInstallCommand(runtime);
308
+ if (await isCommandAvailable(cmd)) {
309
+ await execa(cmd, args, { cwd: projectDir, stdio: "pipe" });
310
+ return { success: true };
311
+ }
312
+ const fallbacks = ["npm", "pnpm", "bun"];
313
+ for (const fb of fallbacks) {
314
+ if (fb === runtime) continue;
315
+ const [fbCmd, fbArgs] = getInstallCommand(fb);
316
+ if (await isCommandAvailable(fbCmd)) {
317
+ await execa(fbCmd, fbArgs, { cwd: projectDir, stdio: "pipe" });
318
+ return { success: true, fallback: fb };
319
+ }
320
+ }
321
+ return { success: false };
322
+ }
323
+
324
+ // src/generators/base.ts
325
+ async function generateBase(config) {
326
+ const { projectDir, projectName, projectDescription, runtime } = config;
327
+ await ensureDir(projectDir);
328
+ await ensureDir(path4.join(projectDir, "apps"));
329
+ await ensureDir(path4.join(projectDir, "apps", "api"));
330
+ await ensureDir(path4.join(projectDir, "apps", "web"));
331
+ const runCmd = getRunCommand(runtime);
332
+ const packageJsonTemplate = getTemplatePath("base", "root", "package.json.ejs");
333
+ const packageJsonContent = await renderTemplate(packageJsonTemplate, {
334
+ projectName,
335
+ projectDescription,
336
+ runCmd
337
+ });
338
+ await writeFile(path4.join(projectDir, "package.json"), packageJsonContent);
339
+ await copyFile(
340
+ getTemplatePath("base", "root", "gitignore"),
341
+ path4.join(projectDir, ".gitignore")
342
+ );
343
+ await copyFile(
344
+ getTemplatePath("base", "root", "editorconfig"),
345
+ path4.join(projectDir, ".editorconfig")
346
+ );
347
+ await copyFile(
348
+ getTemplatePath("base", "root", "nvmrc"),
349
+ path4.join(projectDir, ".nvmrc")
350
+ );
351
+ if (runtime === "pnpm") {
352
+ await copyFile(
353
+ getTemplatePath("base", "root", "pnpm-workspace.yaml"),
354
+ path4.join(projectDir, "pnpm-workspace.yaml")
355
+ );
356
+ }
357
+ }
358
+
359
+ // src/generators/frontend.ts
360
+ import path5 from "path";
361
+
362
+ // src/installers/dependencyVersionMap.ts
363
+ var dependencyVersionMap = {
364
+ // Prisma
365
+ "prisma": "^6.3.0",
366
+ "@prisma/client": "^6.3.0",
367
+ // Mongoose
368
+ "mongoose": "^8.9.0",
369
+ // Auth
370
+ "jsonwebtoken": "^9.0.2",
371
+ "@types/jsonwebtoken": "^9.0.7",
372
+ "bcryptjs": "^2.4.3",
373
+ "@types/bcryptjs": "^2.4.6",
374
+ "passport": "^0.7.0",
375
+ "passport-jwt": "^4.0.1",
376
+ "@types/passport-jwt": "^4.0.1",
377
+ "passport-google-oauth20": "^2.0.0",
378
+ "@types/passport-google-oauth20": "^2.0.16",
379
+ "@nestjs/jwt": "^11.0.0",
380
+ "@nestjs/passport": "^11.0.0",
381
+ "nanoid": "^5.0.9",
382
+ // Queues
383
+ "bullmq": "^5.30.0",
384
+ "@nestjs/bullmq": "^11.0.0",
385
+ "amqplib": "^0.10.5",
386
+ "@types/amqplib": "^0.10.6",
387
+ // Redis
388
+ "ioredis": "^5.4.0",
389
+ // SMTP
390
+ "nodemailer": "^6.9.0",
391
+ "@types/nodemailer": "^6.4.17",
392
+ // Storage
393
+ "minio": "^8.0.0",
394
+ // Integrations
395
+ "axios": "^1.7.0",
396
+ "stripe": "^17.4.0",
397
+ "mercadopago": "^2.0.0",
398
+ // Express
399
+ "express": "^4.21.0",
400
+ "@types/express": "^4.17.21",
401
+ "cors": "^2.8.5",
402
+ "@types/cors": "^2.8.17",
403
+ "helmet": "^8.0.0",
404
+ "dotenv": "^16.4.0",
405
+ // Fastify
406
+ "@fastify/cors": "^10.0.0",
407
+ "@fastify/helmet": "^12.0.0",
408
+ "fastify": "^5.0.0",
409
+ // NestJS
410
+ "@nestjs/common": "^11.0.0",
411
+ "@nestjs/core": "^11.0.0",
412
+ "@nestjs/platform-express": "^11.0.0",
413
+ "@nestjs/config": "^4.0.0",
414
+ "@nestjs/cli": "^11.0.0",
415
+ "reflect-metadata": "^0.2.2",
416
+ "rxjs": "^7.8.1",
417
+ // Next.js
418
+ "next": "^15.0.0",
419
+ "react": "^19.0.0",
420
+ "react-dom": "^19.0.0",
421
+ "@types/react": "^19.0.0",
422
+ "@types/react-dom": "^19.0.0",
423
+ // Vue
424
+ "vue": "^3.5.0",
425
+ "vue-router": "^4.5.0",
426
+ "@vitejs/plugin-vue": "^5.0.0",
427
+ // Angular
428
+ "@angular/core": "^19.0.0",
429
+ "@angular/cli": "^19.0.0",
430
+ // Common
431
+ "typescript": "^5.7.0",
432
+ "vite": "^6.0.0",
433
+ "@types/node": "^22.10.0",
434
+ "tsx": "^4.19.0",
435
+ "pm2": "^5.4.0"
436
+ };
437
+
438
+ // src/generators/frontend.ts
439
+ async function generateFrontend(config, webDir) {
440
+ switch (config.frontend) {
441
+ case "nextjs":
442
+ await generateNextjs(config, webDir);
443
+ break;
444
+ case "react-vite":
445
+ await generateReactVite(config, webDir);
446
+ break;
447
+ case "vue":
448
+ await generateVue(config, webDir);
449
+ break;
450
+ case "angular":
451
+ await generateAngular(config, webDir);
452
+ break;
453
+ }
454
+ }
455
+ async function generateNextjs(config, webDir) {
456
+ await ensureDir(path5.join(webDir, "src", "app"));
457
+ await ensureDir(path5.join(webDir, "public"));
458
+ await writeFile(path5.join(webDir, "package.json"), JSON.stringify({
459
+ name: `@${config.projectName}/web`,
460
+ version: "0.1.0",
461
+ private: true,
462
+ scripts: {
463
+ dev: "next dev",
464
+ build: "next build",
465
+ start: "next start",
466
+ lint: "next lint"
467
+ },
468
+ dependencies: {
469
+ next: dependencyVersionMap.next,
470
+ react: dependencyVersionMap.react,
471
+ "react-dom": dependencyVersionMap["react-dom"]
472
+ },
473
+ devDependencies: {
474
+ "@types/node": dependencyVersionMap["@types/node"],
475
+ "@types/react": dependencyVersionMap["@types/react"],
476
+ "@types/react-dom": dependencyVersionMap["@types/react-dom"],
477
+ typescript: dependencyVersionMap.typescript
478
+ }
479
+ }, null, 2) + "\n");
480
+ await writeFile(path5.join(webDir, "tsconfig.json"), JSON.stringify({
481
+ compilerOptions: {
482
+ target: "ES2017",
483
+ lib: ["dom", "dom.iterable", "esnext"],
484
+ allowJs: true,
485
+ skipLibCheck: true,
486
+ strict: true,
487
+ noEmit: true,
488
+ esModuleInterop: true,
489
+ module: "esnext",
490
+ moduleResolution: "bundler",
491
+ resolveJsonModule: true,
492
+ isolatedModules: true,
493
+ jsx: "preserve",
494
+ incremental: true,
495
+ plugins: [{ name: "next" }],
496
+ paths: { "@/*": ["./src/*"] }
497
+ },
498
+ include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
499
+ exclude: ["node_modules"]
500
+ }, null, 2) + "\n");
501
+ await writeFile(path5.join(webDir, "next.config.ts"), `import type { NextConfig } from 'next';
502
+
503
+ const nextConfig: NextConfig = {};
504
+
505
+ export default nextConfig;
506
+ `);
507
+ await writeFile(path5.join(webDir, "src", "app", "layout.tsx"), `import type { Metadata } from 'next';
508
+ import './globals.css';
509
+
510
+ export const metadata: Metadata = {
511
+ title: '${config.projectName}',
512
+ description: '${config.projectDescription}',
513
+ };
514
+
515
+ export default function RootLayout({
516
+ children,
517
+ }: {
518
+ children: React.ReactNode;
519
+ }) {
520
+ return (
521
+ <html lang="pt-BR">
522
+ <body>{children}</body>
523
+ </html>
524
+ );
525
+ }
526
+ `);
527
+ await writeFile(path5.join(webDir, "src", "app", "page.tsx"), `export default function Home() {
528
+ return (
529
+ <main style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', fontFamily: 'system-ui' }}>
530
+ <h1>${config.projectName}</h1>
531
+ <p>${config.projectDescription}</p>
532
+ <p style={{ marginTop: '2rem', color: '#666' }}>
533
+ API rodando em <a href="http://localhost:3001" style={{ color: '#0070f3' }}>http://localhost:3001</a>
534
+ </p>
535
+ </main>
536
+ );
537
+ }
538
+ `);
539
+ await writeFile(path5.join(webDir, "src", "app", "globals.css"), `* {
540
+ margin: 0;
541
+ padding: 0;
542
+ box-sizing: border-box;
543
+ }
544
+
545
+ body {
546
+ font-family: system-ui, -apple-system, sans-serif;
547
+ -webkit-font-smoothing: antialiased;
548
+ }
549
+
550
+ a {
551
+ color: inherit;
552
+ text-decoration: none;
553
+ }
554
+ `);
555
+ }
556
+ async function generateReactVite(config, webDir) {
557
+ await ensureDir(path5.join(webDir, "src"));
558
+ await ensureDir(path5.join(webDir, "public"));
559
+ await writeFile(path5.join(webDir, "package.json"), JSON.stringify({
560
+ name: `@${config.projectName}/web`,
561
+ version: "0.1.0",
562
+ private: true,
563
+ type: "module",
564
+ scripts: {
565
+ dev: "vite",
566
+ build: "tsc -b && vite build",
567
+ preview: "vite preview"
568
+ },
569
+ dependencies: {
570
+ react: dependencyVersionMap.react,
571
+ "react-dom": dependencyVersionMap["react-dom"]
572
+ },
573
+ devDependencies: {
574
+ "@types/react": dependencyVersionMap["@types/react"],
575
+ "@types/react-dom": dependencyVersionMap["@types/react-dom"],
576
+ "@vitejs/plugin-react": "^4.3.0",
577
+ typescript: dependencyVersionMap.typescript,
578
+ vite: dependencyVersionMap.vite
579
+ }
580
+ }, null, 2) + "\n");
581
+ await writeFile(path5.join(webDir, "tsconfig.json"), JSON.stringify({
582
+ compilerOptions: {
583
+ target: "ES2020",
584
+ useDefineForClassFields: true,
585
+ lib: ["ES2020", "DOM", "DOM.Iterable"],
586
+ module: "ESNext",
587
+ skipLibCheck: true,
588
+ moduleResolution: "bundler",
589
+ allowImportingTsExtensions: true,
590
+ isolatedModules: true,
591
+ noEmit: true,
592
+ jsx: "react-jsx",
593
+ strict: true
594
+ },
595
+ include: ["src"]
596
+ }, null, 2) + "\n");
597
+ await writeFile(path5.join(webDir, "vite.config.ts"), `import { defineConfig } from 'vite';
598
+ import react from '@vitejs/plugin-react';
599
+
600
+ export default defineConfig({
601
+ plugins: [react()],
602
+ server: {
603
+ port: 3000,
604
+ },
605
+ });
606
+ `);
607
+ await writeFile(path5.join(webDir, "index.html"), `<!doctype html>
608
+ <html lang="pt-BR">
609
+ <head>
610
+ <meta charset="UTF-8" />
611
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
612
+ <title>${config.projectName}</title>
613
+ </head>
614
+ <body>
615
+ <div id="root"></div>
616
+ <script type="module" src="/src/main.tsx"></script>
617
+ </body>
618
+ </html>
619
+ `);
620
+ await writeFile(path5.join(webDir, "src", "main.tsx"), `import React from 'react';
621
+ import ReactDOM from 'react-dom/client';
622
+ import App from './App';
623
+ import './index.css';
624
+
625
+ ReactDOM.createRoot(document.getElementById('root')!).render(
626
+ <React.StrictMode>
627
+ <App />
628
+ </React.StrictMode>,
629
+ );
630
+ `);
631
+ await writeFile(path5.join(webDir, "src", "App.tsx"), `function App() {
632
+ return (
633
+ <main style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', fontFamily: 'system-ui' }}>
634
+ <h1>${config.projectName}</h1>
635
+ <p>${config.projectDescription}</p>
636
+ <p style={{ marginTop: '2rem', color: '#666' }}>
637
+ API rodando em <a href="http://localhost:3001" style={{ color: '#0070f3' }}>http://localhost:3001</a>
638
+ </p>
639
+ </main>
640
+ );
641
+ }
642
+
643
+ export default App;
644
+ `);
645
+ await writeFile(path5.join(webDir, "src", "index.css"), `* {
646
+ margin: 0;
647
+ padding: 0;
648
+ box-sizing: border-box;
649
+ }
650
+
651
+ body {
652
+ font-family: system-ui, -apple-system, sans-serif;
653
+ -webkit-font-smoothing: antialiased;
654
+ }
655
+ `);
656
+ }
657
+ async function generateVue(config, webDir) {
658
+ await ensureDir(path5.join(webDir, "src"));
659
+ await ensureDir(path5.join(webDir, "public"));
660
+ await writeFile(path5.join(webDir, "package.json"), JSON.stringify({
661
+ name: `@${config.projectName}/web`,
662
+ version: "0.1.0",
663
+ private: true,
664
+ type: "module",
665
+ scripts: {
666
+ dev: "vite",
667
+ build: "vue-tsc -b && vite build",
668
+ preview: "vite preview"
669
+ },
670
+ dependencies: {
671
+ vue: dependencyVersionMap.vue,
672
+ "vue-router": dependencyVersionMap["vue-router"]
673
+ },
674
+ devDependencies: {
675
+ "@vitejs/plugin-vue": dependencyVersionMap["@vitejs/plugin-vue"],
676
+ typescript: dependencyVersionMap.typescript,
677
+ vite: dependencyVersionMap.vite,
678
+ "vue-tsc": "^2.1.0"
679
+ }
680
+ }, null, 2) + "\n");
681
+ await writeFile(path5.join(webDir, "tsconfig.json"), JSON.stringify({
682
+ compilerOptions: {
683
+ target: "ES2020",
684
+ module: "ESNext",
685
+ lib: ["ES2020", "DOM", "DOM.Iterable"],
686
+ skipLibCheck: true,
687
+ moduleResolution: "bundler",
688
+ allowImportingTsExtensions: true,
689
+ isolatedModules: true,
690
+ noEmit: true,
691
+ jsx: "preserve",
692
+ strict: true
693
+ },
694
+ include: ["src/**/*.ts", "src/**/*.vue"]
695
+ }, null, 2) + "\n");
696
+ await writeFile(path5.join(webDir, "vite.config.ts"), `import { defineConfig } from 'vite';
697
+ import vue from '@vitejs/plugin-vue';
698
+
699
+ export default defineConfig({
700
+ plugins: [vue()],
701
+ server: {
702
+ port: 3000,
703
+ },
704
+ });
705
+ `);
706
+ await writeFile(path5.join(webDir, "index.html"), `<!doctype html>
707
+ <html lang="pt-BR">
708
+ <head>
709
+ <meta charset="UTF-8" />
710
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
711
+ <title>${config.projectName}</title>
712
+ </head>
713
+ <body>
714
+ <div id="app"></div>
715
+ <script type="module" src="/src/main.ts"></script>
716
+ </body>
717
+ </html>
718
+ `);
719
+ await writeFile(path5.join(webDir, "src", "main.ts"), `import { createApp } from 'vue';
720
+ import App from './App.vue';
721
+ import './style.css';
722
+
723
+ createApp(App).mount('#app');
724
+ `);
725
+ await writeFile(path5.join(webDir, "src", "App.vue"), `<template>
726
+ <main class="container">
727
+ <h1>${config.projectName}</h1>
728
+ <p>${config.projectDescription}</p>
729
+ <p class="api-link">
730
+ API rodando em <a href="http://localhost:3001">http://localhost:3001</a>
731
+ </p>
732
+ </main>
733
+ </template>
734
+
735
+ <style scoped>
736
+ .container {
737
+ display: flex;
738
+ flex-direction: column;
739
+ align-items: center;
740
+ justify-content: center;
741
+ min-height: 100vh;
742
+ font-family: system-ui;
743
+ }
744
+ .api-link {
745
+ margin-top: 2rem;
746
+ color: #666;
747
+ }
748
+ .api-link a {
749
+ color: #42b883;
750
+ }
751
+ </style>
752
+ `);
753
+ await writeFile(path5.join(webDir, "src", "style.css"), `* {
754
+ margin: 0;
755
+ padding: 0;
756
+ box-sizing: border-box;
757
+ }
758
+
759
+ body {
760
+ font-family: system-ui, -apple-system, sans-serif;
761
+ -webkit-font-smoothing: antialiased;
762
+ }
763
+ `);
764
+ await writeFile(path5.join(webDir, "env.d.ts"), `/// <reference types="vite/client" />
765
+ `);
766
+ }
767
+ async function generateAngular(config, webDir) {
768
+ await ensureDir(path5.join(webDir, "src", "app"));
769
+ await writeFile(path5.join(webDir, "package.json"), JSON.stringify({
770
+ name: `@${config.projectName}/web`,
771
+ version: "0.1.0",
772
+ private: true,
773
+ scripts: {
774
+ dev: "ng serve --port 3000",
775
+ build: "ng build",
776
+ lint: "ng lint"
777
+ },
778
+ dependencies: {
779
+ "@angular/animations": dependencyVersionMap["@angular/core"],
780
+ "@angular/common": dependencyVersionMap["@angular/core"],
781
+ "@angular/compiler": dependencyVersionMap["@angular/core"],
782
+ "@angular/core": dependencyVersionMap["@angular/core"],
783
+ "@angular/forms": dependencyVersionMap["@angular/core"],
784
+ "@angular/platform-browser": dependencyVersionMap["@angular/core"],
785
+ "@angular/platform-browser-dynamic": dependencyVersionMap["@angular/core"],
786
+ "@angular/router": dependencyVersionMap["@angular/core"],
787
+ rxjs: dependencyVersionMap.rxjs,
788
+ "zone.js": "^0.15.0"
789
+ },
790
+ devDependencies: {
791
+ "@angular/cli": dependencyVersionMap["@angular/cli"],
792
+ "@angular/compiler-cli": dependencyVersionMap["@angular/core"],
793
+ typescript: dependencyVersionMap.typescript
794
+ }
795
+ }, null, 2) + "\n");
796
+ await writeFile(path5.join(webDir, "tsconfig.json"), JSON.stringify({
797
+ compilerOptions: {
798
+ target: "ES2022",
799
+ module: "ES2022",
800
+ lib: ["ES2022", "dom"],
801
+ skipLibCheck: true,
802
+ moduleResolution: "bundler",
803
+ strict: true,
804
+ noEmit: false,
805
+ declaration: false,
806
+ experimentalDecorators: true,
807
+ emitDecoratorMetadata: true,
808
+ outDir: "./dist"
809
+ },
810
+ include: ["src/**/*.ts"]
811
+ }, null, 2) + "\n");
812
+ await writeFile(path5.join(webDir, "angular.json"), JSON.stringify({
813
+ $schema: "./node_modules/@angular/cli/lib/config/schema.json",
814
+ version: 1,
815
+ newProjectRoot: "projects",
816
+ projects: {
817
+ web: {
818
+ projectType: "application",
819
+ root: "",
820
+ sourceRoot: "src",
821
+ architect: {
822
+ build: {
823
+ builder: "@angular-devkit/build-angular:application",
824
+ options: {
825
+ outputPath: "dist",
826
+ index: "src/index.html",
827
+ browser: "src/main.ts",
828
+ tsConfig: "tsconfig.json"
829
+ }
830
+ },
831
+ serve: {
832
+ builder: "@angular-devkit/build-angular:dev-server",
833
+ configurations: {
834
+ development: { buildTarget: "web:build" }
835
+ },
836
+ defaultConfiguration: "development"
837
+ }
838
+ }
839
+ }
840
+ }
841
+ }, null, 2) + "\n");
842
+ await writeFile(path5.join(webDir, "src", "index.html"), `<!doctype html>
843
+ <html lang="pt-BR">
844
+ <head>
845
+ <meta charset="UTF-8" />
846
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
847
+ <title>${config.projectName}</title>
848
+ </head>
849
+ <body>
850
+ <app-root></app-root>
851
+ </body>
852
+ </html>
853
+ `);
854
+ await writeFile(path5.join(webDir, "src", "main.ts"), `import { bootstrapApplication } from '@angular/platform-browser';
855
+ import { AppComponent } from './app/app.component';
856
+
857
+ bootstrapApplication(AppComponent).catch((err) => console.error(err));
858
+ `);
859
+ await writeFile(path5.join(webDir, "src", "app", "app.component.ts"), `import { Component } from '@angular/core';
860
+
861
+ @Component({
862
+ selector: 'app-root',
863
+ standalone: true,
864
+ template: \`
865
+ <main style="display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; font-family: system-ui;">
866
+ <h1>${config.projectName}</h1>
867
+ <p>${config.projectDescription}</p>
868
+ <p style="margin-top: 2rem; color: #666;">
869
+ API rodando em <a href="http://localhost:3001" style="color: #dd0031;">http://localhost:3001</a>
870
+ </p>
871
+ </main>
872
+ \`,
873
+ })
874
+ export class AppComponent {}
875
+ `);
876
+ await writeFile(path5.join(webDir, "src", "styles.css"), `* {
877
+ margin: 0;
878
+ padding: 0;
879
+ box-sizing: border-box;
880
+ }
881
+
882
+ body {
883
+ font-family: system-ui, -apple-system, sans-serif;
884
+ -webkit-font-smoothing: antialiased;
885
+ }
886
+ `);
887
+ }
888
+
889
+ // src/generators/backend.ts
890
+ import path6 from "path";
891
+ async function generateBackend(config, apiDir) {
892
+ switch (config.backend) {
893
+ case "express":
894
+ await generateExpress(config, apiDir);
895
+ break;
896
+ case "fastify":
897
+ await generateFastify(config, apiDir);
898
+ break;
899
+ case "nestjs":
900
+ await generateNestjs(config, apiDir);
901
+ break;
902
+ }
903
+ }
904
+ async function generateExpress(config, apiDir) {
905
+ await ensureDir(path6.join(apiDir, "src", "routes"));
906
+ await ensureDir(path6.join(apiDir, "src", "middlewares"));
907
+ await ensureDir(path6.join(apiDir, "src", "config"));
908
+ await writeFile(path6.join(apiDir, "package.json"), JSON.stringify({
909
+ name: `@${config.projectName}/api`,
910
+ version: "0.1.0",
911
+ private: true,
912
+ type: "module",
913
+ scripts: {
914
+ dev: "tsx watch src/index.ts",
915
+ build: "tsc",
916
+ start: "node dist/index.js",
917
+ lint: "tsc --noEmit"
918
+ },
919
+ dependencies: {
920
+ express: dependencyVersionMap.express,
921
+ cors: dependencyVersionMap.cors,
922
+ helmet: dependencyVersionMap.helmet,
923
+ dotenv: dependencyVersionMap.dotenv
924
+ },
925
+ devDependencies: {
926
+ "@types/express": dependencyVersionMap["@types/express"],
927
+ "@types/cors": dependencyVersionMap["@types/cors"],
928
+ "@types/node": dependencyVersionMap["@types/node"],
929
+ typescript: dependencyVersionMap.typescript,
930
+ tsx: dependencyVersionMap.tsx
931
+ }
932
+ }, null, 2) + "\n");
933
+ await writeFile(path6.join(apiDir, "tsconfig.json"), JSON.stringify({
934
+ compilerOptions: {
935
+ target: "ES2022",
936
+ module: "ESNext",
937
+ moduleResolution: "bundler",
938
+ esModuleInterop: true,
939
+ strict: true,
940
+ outDir: "dist",
941
+ rootDir: "src",
942
+ skipLibCheck: true,
943
+ declaration: true
944
+ },
945
+ include: ["src/**/*"],
946
+ exclude: ["node_modules", "dist"]
947
+ }, null, 2) + "\n");
948
+ await writeFile(path6.join(apiDir, "src", "config", "index.ts"), `import 'dotenv/config';
949
+
950
+ export const config = {
951
+ port: Number(process.env.PORT) || 3001,
952
+ nodeEnv: process.env.NODE_ENV || 'development',
953
+ corsOrigin: process.env.CORS_ORIGIN || 'http://localhost:3000',
954
+ };
955
+ `);
956
+ await writeFile(path6.join(apiDir, "src", "app.ts"), `import express from 'express';
957
+ import cors from 'cors';
958
+ import helmet from 'helmet';
959
+ import { config } from './config/index.js';
960
+ import { router } from './routes/index.js';
961
+ import { errorHandler } from './middlewares/errorHandler.js';
962
+
963
+ const app = express();
964
+
965
+ app.use(helmet());
966
+ app.use(cors({ origin: config.corsOrigin }));
967
+ app.use(express.json());
968
+
969
+ app.use('/api', router);
970
+
971
+ app.use(errorHandler);
972
+
973
+ export { app };
974
+ `);
975
+ await writeFile(path6.join(apiDir, "src", "index.ts"), `import { app } from './app.js';
976
+ import { config } from './config/index.js';
977
+
978
+ app.listen(config.port, () => {
979
+ console.log(\`\u{1F680} API rodando em http://localhost:\${config.port}\`);
980
+ });
981
+ `);
982
+ await writeFile(path6.join(apiDir, "src", "routes", "index.ts"), `import { Router } from 'express';
983
+
984
+ const router = Router();
985
+
986
+ router.get('/health', (_req, res) => {
987
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
988
+ });
989
+
990
+ export { router };
991
+ `);
992
+ await writeFile(path6.join(apiDir, "src", "middlewares", "errorHandler.ts"), `import type { Request, Response, NextFunction } from 'express';
993
+
994
+ export function errorHandler(err: Error, _req: Request, res: Response, _next: NextFunction) {
995
+ console.error(err.stack);
996
+ res.status(500).json({
997
+ error: 'Internal Server Error',
998
+ message: process.env.NODE_ENV === 'development' ? err.message : undefined,
999
+ });
1000
+ }
1001
+ `);
1002
+ }
1003
+ async function generateFastify(config, apiDir) {
1004
+ await ensureDir(path6.join(apiDir, "src", "routes"));
1005
+ await ensureDir(path6.join(apiDir, "src", "plugins"));
1006
+ await ensureDir(path6.join(apiDir, "src", "config"));
1007
+ await writeFile(path6.join(apiDir, "package.json"), JSON.stringify({
1008
+ name: `@${config.projectName}/api`,
1009
+ version: "0.1.0",
1010
+ private: true,
1011
+ type: "module",
1012
+ scripts: {
1013
+ dev: "tsx watch src/index.ts",
1014
+ build: "tsc",
1015
+ start: "node dist/index.js",
1016
+ lint: "tsc --noEmit"
1017
+ },
1018
+ dependencies: {
1019
+ fastify: dependencyVersionMap.fastify,
1020
+ "@fastify/cors": dependencyVersionMap["@fastify/cors"],
1021
+ "@fastify/helmet": dependencyVersionMap["@fastify/helmet"],
1022
+ dotenv: dependencyVersionMap.dotenv
1023
+ },
1024
+ devDependencies: {
1025
+ "@types/node": dependencyVersionMap["@types/node"],
1026
+ typescript: dependencyVersionMap.typescript,
1027
+ tsx: dependencyVersionMap.tsx
1028
+ }
1029
+ }, null, 2) + "\n");
1030
+ await writeFile(path6.join(apiDir, "tsconfig.json"), JSON.stringify({
1031
+ compilerOptions: {
1032
+ target: "ES2022",
1033
+ module: "ESNext",
1034
+ moduleResolution: "bundler",
1035
+ esModuleInterop: true,
1036
+ strict: true,
1037
+ outDir: "dist",
1038
+ rootDir: "src",
1039
+ skipLibCheck: true,
1040
+ declaration: true
1041
+ },
1042
+ include: ["src/**/*"],
1043
+ exclude: ["node_modules", "dist"]
1044
+ }, null, 2) + "\n");
1045
+ await writeFile(path6.join(apiDir, "src", "config", "index.ts"), `import 'dotenv/config';
1046
+
1047
+ export const config = {
1048
+ port: Number(process.env.PORT) || 3001,
1049
+ host: process.env.HOST || '0.0.0.0',
1050
+ nodeEnv: process.env.NODE_ENV || 'development',
1051
+ corsOrigin: process.env.CORS_ORIGIN || 'http://localhost:3000',
1052
+ };
1053
+ `);
1054
+ await writeFile(path6.join(apiDir, "src", "app.ts"), `import Fastify from 'fastify';
1055
+ import cors from '@fastify/cors';
1056
+ import helmet from '@fastify/helmet';
1057
+ import { config } from './config/index.js';
1058
+ import { registerRoutes } from './routes/index.js';
1059
+
1060
+ export async function buildApp() {
1061
+ const app = Fastify({
1062
+ logger: config.nodeEnv === 'development',
1063
+ });
1064
+
1065
+ await app.register(helmet);
1066
+ await app.register(cors, { origin: config.corsOrigin });
1067
+
1068
+ registerRoutes(app);
1069
+
1070
+ return app;
1071
+ }
1072
+ `);
1073
+ await writeFile(path6.join(apiDir, "src", "index.ts"), `import { buildApp } from './app.js';
1074
+ import { config } from './config/index.js';
1075
+
1076
+ async function start() {
1077
+ const app = await buildApp();
1078
+
1079
+ await app.listen({ port: config.port, host: config.host });
1080
+ console.log(\`\u{1F680} API rodando em http://localhost:\${config.port}\`);
1081
+ }
1082
+
1083
+ start().catch((err) => {
1084
+ console.error(err);
1085
+ process.exit(1);
1086
+ });
1087
+ `);
1088
+ await writeFile(path6.join(apiDir, "src", "routes", "index.ts"), `import type { FastifyInstance } from 'fastify';
1089
+
1090
+ export function registerRoutes(app: FastifyInstance) {
1091
+ app.get('/api/health', async () => {
1092
+ return { status: 'ok', timestamp: new Date().toISOString() };
1093
+ });
1094
+ }
1095
+ `);
1096
+ }
1097
+ async function generateNestjs(config, apiDir) {
1098
+ await ensureDir(path6.join(apiDir, "src"));
1099
+ await writeFile(path6.join(apiDir, "package.json"), JSON.stringify({
1100
+ name: `@${config.projectName}/api`,
1101
+ version: "0.1.0",
1102
+ private: true,
1103
+ scripts: {
1104
+ dev: "nest start --watch",
1105
+ build: "nest build",
1106
+ start: "node dist/main.js",
1107
+ "start:prod": "node dist/main.js",
1108
+ lint: "tsc --noEmit"
1109
+ },
1110
+ dependencies: {
1111
+ "@nestjs/common": dependencyVersionMap["@nestjs/common"],
1112
+ "@nestjs/core": dependencyVersionMap["@nestjs/core"],
1113
+ "@nestjs/platform-express": dependencyVersionMap["@nestjs/platform-express"],
1114
+ "@nestjs/config": dependencyVersionMap["@nestjs/config"],
1115
+ "reflect-metadata": dependencyVersionMap["reflect-metadata"],
1116
+ rxjs: dependencyVersionMap.rxjs
1117
+ },
1118
+ devDependencies: {
1119
+ "@nestjs/cli": dependencyVersionMap["@nestjs/cli"],
1120
+ "@types/node": dependencyVersionMap["@types/node"],
1121
+ typescript: dependencyVersionMap.typescript
1122
+ }
1123
+ }, null, 2) + "\n");
1124
+ await writeFile(path6.join(apiDir, "tsconfig.json"), JSON.stringify({
1125
+ compilerOptions: {
1126
+ module: "commonjs",
1127
+ declaration: true,
1128
+ removeComments: true,
1129
+ emitDecoratorMetadata: true,
1130
+ experimentalDecorators: true,
1131
+ allowSyntheticDefaultImports: true,
1132
+ target: "ES2021",
1133
+ sourceMap: true,
1134
+ outDir: "./dist",
1135
+ rootDir: "./src",
1136
+ strict: true,
1137
+ skipLibCheck: true
1138
+ },
1139
+ include: ["src/**/*"],
1140
+ exclude: ["node_modules", "dist"]
1141
+ }, null, 2) + "\n");
1142
+ await writeFile(path6.join(apiDir, "nest-cli.json"), JSON.stringify({
1143
+ $schema: "https://json.schemastore.org/nest-cli",
1144
+ collection: "@nestjs/schematics",
1145
+ sourceRoot: "src"
1146
+ }, null, 2) + "\n");
1147
+ await writeFile(path6.join(apiDir, "src", "main.ts"), `import { NestFactory } from '@nestjs/core';
1148
+ import { AppModule } from './app.module';
1149
+
1150
+ async function bootstrap() {
1151
+ const app = await NestFactory.create(AppModule);
1152
+ app.enableCors({ origin: process.env.CORS_ORIGIN || 'http://localhost:3000' });
1153
+ app.setGlobalPrefix('api');
1154
+
1155
+ const port = process.env.PORT || 3001;
1156
+ await app.listen(port);
1157
+ console.log(\`\u{1F680} API rodando em http://localhost:\${port}\`);
1158
+ }
1159
+
1160
+ bootstrap();
1161
+ `);
1162
+ await writeFile(path6.join(apiDir, "src", "app.module.ts"), `import { Module } from '@nestjs/common';
1163
+ import { ConfigModule } from '@nestjs/config';
1164
+ import { AppController } from './app.controller';
1165
+ import { AppService } from './app.service';
1166
+
1167
+ @Module({
1168
+ imports: [
1169
+ ConfigModule.forRoot({ isGlobal: true }),
1170
+ ],
1171
+ controllers: [AppController],
1172
+ providers: [AppService],
1173
+ })
1174
+ export class AppModule {}
1175
+ `);
1176
+ await writeFile(path6.join(apiDir, "src", "app.controller.ts"), `import { Controller, Get } from '@nestjs/common';
1177
+ import { AppService } from './app.service';
1178
+
1179
+ @Controller()
1180
+ export class AppController {
1181
+ constructor(private readonly appService: AppService) {}
1182
+
1183
+ @Get('health')
1184
+ getHealth() {
1185
+ return this.appService.getHealth();
1186
+ }
1187
+ }
1188
+ `);
1189
+ await writeFile(path6.join(apiDir, "src", "app.service.ts"), `import { Injectable } from '@nestjs/common';
1190
+
1191
+ @Injectable()
1192
+ export class AppService {
1193
+ getHealth() {
1194
+ return { status: 'ok', timestamp: new Date().toISOString() };
1195
+ }
1196
+ }
1197
+ `);
1198
+ }
1199
+
1200
+ // src/generators/docker.ts
1201
+ import path7 from "path";
1202
+ async function generateDocker(config, projectDir, extraServices) {
1203
+ const services = [];
1204
+ if (config.database === "postgresql") {
1205
+ services.push(` postgres:
1206
+ image: postgres:16-alpine
1207
+ container_name: ${config.projectName}-postgres
1208
+ restart: unless-stopped
1209
+ environment:
1210
+ POSTGRES_USER: postgres
1211
+ POSTGRES_PASSWORD: postgres
1212
+ POSTGRES_DB: ${config.projectName.replace(/-/g, "_")}
1213
+ ports:
1214
+ - "5432:5432"
1215
+ volumes:
1216
+ - postgres_data:/var/lib/postgresql/data`);
1217
+ }
1218
+ if (config.database === "mysql") {
1219
+ services.push(` mysql:
1220
+ image: mysql:8
1221
+ container_name: ${config.projectName}-mysql
1222
+ restart: unless-stopped
1223
+ environment:
1224
+ MYSQL_ROOT_PASSWORD: root
1225
+ MYSQL_DATABASE: ${config.projectName.replace(/-/g, "_")}
1226
+ MYSQL_USER: user
1227
+ MYSQL_PASSWORD: password
1228
+ ports:
1229
+ - "3306:3306"
1230
+ volumes:
1231
+ - mysql_data:/var/lib/mysql`);
1232
+ }
1233
+ if (config.database === "mongodb") {
1234
+ services.push(` mongodb:
1235
+ image: mongo:7
1236
+ container_name: ${config.projectName}-mongodb
1237
+ restart: unless-stopped
1238
+ environment:
1239
+ MONGO_INITDB_ROOT_USERNAME: root
1240
+ MONGO_INITDB_ROOT_PASSWORD: root
1241
+ MONGO_INITDB_DATABASE: ${config.projectName.replace(/-/g, "_")}
1242
+ ports:
1243
+ - "27017:27017"
1244
+ volumes:
1245
+ - mongodb_data:/data/db`);
1246
+ }
1247
+ if (config.redis) {
1248
+ services.push(` redis:
1249
+ image: redis:7-alpine
1250
+ container_name: ${config.projectName}-redis
1251
+ restart: unless-stopped
1252
+ ports:
1253
+ - "6379:6379"
1254
+ volumes:
1255
+ - redis_data:/data`);
1256
+ }
1257
+ if (config.queue === "rabbitmq") {
1258
+ services.push(` rabbitmq:
1259
+ image: rabbitmq:3-management-alpine
1260
+ container_name: ${config.projectName}-rabbitmq
1261
+ restart: unless-stopped
1262
+ environment:
1263
+ RABBITMQ_DEFAULT_USER: guest
1264
+ RABBITMQ_DEFAULT_PASS: guest
1265
+ ports:
1266
+ - "5672:5672"
1267
+ - "15672:15672"
1268
+ volumes:
1269
+ - rabbitmq_data:/var/lib/rabbitmq`);
1270
+ }
1271
+ if (config.minio) {
1272
+ services.push(` minio:
1273
+ image: minio/minio
1274
+ container_name: ${config.projectName}-minio
1275
+ restart: unless-stopped
1276
+ command: server /data --console-address ":9001"
1277
+ environment:
1278
+ MINIO_ROOT_USER: minioadmin
1279
+ MINIO_ROOT_PASSWORD: minioadmin
1280
+ ports:
1281
+ - "9000:9000"
1282
+ - "9001:9001"
1283
+ volumes:
1284
+ - minio_data:/data`);
1285
+ }
1286
+ services.push(...extraServices);
1287
+ const volumes = [];
1288
+ if (config.database === "postgresql") volumes.push(" postgres_data:");
1289
+ if (config.database === "mysql") volumes.push(" mysql_data:");
1290
+ if (config.database === "mongodb") volumes.push(" mongodb_data:");
1291
+ if (config.redis) volumes.push(" redis_data:");
1292
+ if (config.queue === "rabbitmq") volumes.push(" rabbitmq_data:");
1293
+ if (config.minio) volumes.push(" minio_data:");
1294
+ const content = `services:
1295
+ ${services.join("\n\n")}
1296
+
1297
+ volumes:
1298
+ ${volumes.join("\n")}
1299
+ `;
1300
+ await writeFile(path7.join(projectDir, "docker-compose.yml"), content);
1301
+ }
1302
+
1303
+ // src/generators/env.ts
1304
+ import path8 from "path";
1305
+ async function generateEnv(config, apiDir, envEntries) {
1306
+ const baseEntries = [
1307
+ { key: "NODE_ENV", value: "development", category: "App", comment: "Ambiente" },
1308
+ { key: "PORT", value: "3001", category: "App" }
1309
+ ];
1310
+ if (config.database === "postgresql") {
1311
+ baseEntries.push({
1312
+ key: "DATABASE_URL",
1313
+ value: `postgresql://postgres:postgres@localhost:5432/${config.projectName.replace(/-/g, "_")}`,
1314
+ category: "Database"
1315
+ });
1316
+ } else if (config.database === "mysql") {
1317
+ baseEntries.push({
1318
+ key: "DATABASE_URL",
1319
+ value: `mysql://user:password@localhost:3306/${config.projectName.replace(/-/g, "_")}`,
1320
+ category: "Database"
1321
+ });
1322
+ } else if (config.database === "mongodb") {
1323
+ baseEntries.push({
1324
+ key: "MONGODB_URI",
1325
+ value: `mongodb://root:root@localhost:27017/${config.projectName.replace(/-/g, "_")}?authSource=admin`,
1326
+ category: "Database"
1327
+ });
1328
+ }
1329
+ const allEntries = [...baseEntries, ...envEntries];
1330
+ const categories = /* @__PURE__ */ new Map();
1331
+ for (const entry of allEntries) {
1332
+ const cat = entry.category;
1333
+ if (!categories.has(cat)) categories.set(cat, []);
1334
+ categories.get(cat).push(entry);
1335
+ }
1336
+ const lines = [];
1337
+ for (const [category, entries] of categories) {
1338
+ lines.push(`# ${category}`);
1339
+ for (const entry of entries) {
1340
+ if (entry.comment) lines.push(`# ${entry.comment}`);
1341
+ lines.push(`${entry.key}="${entry.value}"`);
1342
+ }
1343
+ lines.push("");
1344
+ }
1345
+ const envContent = lines.join("\n");
1346
+ await writeFile(path8.join(apiDir, ".env"), envContent);
1347
+ await writeFile(path8.join(apiDir, ".env.example"), envContent);
1348
+ }
1349
+
1350
+ // src/generators/readme.ts
1351
+ import path9 from "path";
1352
+ async function generateReadme(config, projectDir) {
1353
+ const runCmd = getRunCommand(config.runtime);
1354
+ const hasDocker = config.database !== "none" || config.redis || config.queue === "rabbitmq" || config.minio;
1355
+ const authMethods = [
1356
+ config.auth.jwt && "JWT",
1357
+ config.auth.magicLink && "Magic Link",
1358
+ config.auth.googleOAuth && "Google OAuth"
1359
+ ].filter(Boolean);
1360
+ const integrations = [
1361
+ config.integrations.viacep && "ViaCEP",
1362
+ config.integrations.whatsapp && "WhatsApp",
1363
+ config.integrations.stripe && "Stripe",
1364
+ config.integrations.mercadoPago && "Mercado Pago",
1365
+ config.integrations.abacatePay && "AbacatePay"
1366
+ ].filter(Boolean);
1367
+ const infra = [
1368
+ config.redis && "Redis",
1369
+ config.smtp && "SMTP",
1370
+ config.minio && "MinIO",
1371
+ config.pm2 && "PM2"
1372
+ ].filter(Boolean);
1373
+ let content = `# ${config.projectName}
1374
+
1375
+ ${config.projectDescription}
1376
+
1377
+ > Projeto gerado com [PlazerCLI](https://github.com/pablocarss/plazercli) + Liz AI Agent
1378
+
1379
+ ## Stack
1380
+
1381
+ | Camada | Tecnologia |
1382
+ |--------|-----------|
1383
+ | Frontend | ${config.frontend} |
1384
+ | Backend | ${config.backend} |
1385
+ ${config.database !== "none" ? `| Banco de dados | ${config.database} ${config.usePrisma ? "(Prisma)" : "(Mongoose)"} |` : ""}
1386
+ ${authMethods.length > 0 ? `| Autenticacao | ${authMethods.join(", ")} |` : ""}
1387
+ ${config.queue !== "none" ? `| Filas | ${config.queue} |` : ""}
1388
+ ${infra.length > 0 ? `| Infraestrutura | ${infra.join(", ")} |` : ""}
1389
+ ${integrations.length > 0 ? `| Integracoes | ${integrations.join(", ")} |` : ""}
1390
+ | Runtime | ${config.runtime} |
1391
+
1392
+ ## Estrutura
1393
+
1394
+ \`\`\`
1395
+ ${config.projectName}/
1396
+ \u251C\u2500\u2500 apps/
1397
+ \u2502 \u251C\u2500\u2500 api/ # Backend ${config.backend}
1398
+ \u2502 \u2502 \u251C\u2500\u2500 src/
1399
+ ${config.usePrisma ? "\u2502 \u2502 \u2502 \u251C\u2500\u2500 database/ # Prisma service\n" : ""}${config.useMongoose ? "\u2502 \u2502 \u2502 \u251C\u2500\u2500 database/ # Mongoose models\n" : ""}${config.hasAnyAuth ? "\u2502 \u2502 \u2502 \u251C\u2500\u2500 auth/ # Autenticacao\n" : ""}${config.redis || config.smtp || config.minio ? "\u2502 \u2502 \u2502 \u251C\u2500\u2500 infra/ # Redis, Email, Storage\n" : ""}${config.hasAnyQueue ? "\u2502 \u2502 \u2502 \u251C\u2500\u2500 queue/ # Workers e jobs\n" : ""}${config.hasAnyIntegration ? "\u2502 \u2502 \u2502 \u251C\u2500\u2500 integrations/ # APIs externas\n" : ""}${config.multiTenant ? "\u2502 \u2502 \u2502 \u251C\u2500\u2500 tenant/ # Multi-tenant\n" : ""}\u2502 \u2502 \u2502 \u251C\u2500\u2500 routes/
1400
+ \u2502 \u2502 \u2502 \u2514\u2500\u2500 config/
1401
+ ${config.usePrisma ? "\u2502 \u2502 \u2514\u2500\u2500 prisma/schema.prisma\n" : ""}\u2502 \u2502 \u2514\u2500\u2500 .env
1402
+ \u2502 \u2514\u2500\u2500 web/ # Frontend ${config.frontend}
1403
+ \u2502 \u2514\u2500\u2500 src/
1404
+ \u251C\u2500\u2500 docker-compose.yml
1405
+ \u251C\u2500\u2500 CLAUDE.md # Liz config (Claude Code)
1406
+ \u251C\u2500\u2500 AGENTS.md # Liz config (geral)
1407
+ \u2514\u2500\u2500 README.md
1408
+ \`\`\`
1409
+
1410
+ ## Quick Start
1411
+
1412
+ \`\`\`bash
1413
+ # Pre-requisitos: Node.js >= 20, ${config.runtime}${hasDocker ? ", Docker" : ""}
1414
+ `;
1415
+ let step = 1;
1416
+ if (hasDocker) {
1417
+ content += `
1418
+ # ${step}. Subir infraestrutura
1419
+ docker compose up -d
1420
+ `;
1421
+ step++;
1422
+ }
1423
+ if (config.usePrisma) {
1424
+ content += `
1425
+ # ${step}. Criar tabelas no banco
1426
+ cd apps/api && npx prisma db push && cd ../..
1427
+ `;
1428
+ step++;
1429
+ }
1430
+ content += `
1431
+ # ${step}. Iniciar em modo desenvolvimento
1432
+ ${runCmd} dev
1433
+ \`\`\`
1434
+
1435
+ Pronto! Frontend em **http://localhost:3000** e API em **http://localhost:3001**
1436
+ `;
1437
+ if (config.hasAnyAuth) {
1438
+ content += `
1439
+ ## Autenticacao
1440
+
1441
+ ### Endpoints
1442
+ `;
1443
+ if (config.auth.jwt) {
1444
+ content += `
1445
+ **JWT:**
1446
+ - \`POST /api/auth/register\` - Criar conta
1447
+ \`\`\`json
1448
+ { "email": "user@email.com", "password": "123456", "name": "Nome" }
1449
+ \`\`\`
1450
+ - \`POST /api/auth/login\` - Login (retorna access_token)
1451
+ \`\`\`json
1452
+ { "email": "user@email.com", "password": "123456" }
1453
+ \`\`\`
1454
+ - Header: \`Authorization: Bearer <token>\`
1455
+ `;
1456
+ }
1457
+ if (config.auth.magicLink) {
1458
+ content += `
1459
+ **Magic Link:**
1460
+ - \`POST /api/auth/magic-link/send\` - Enviar link por email
1461
+ \`\`\`json
1462
+ { "email": "user@email.com" }
1463
+ \`\`\`
1464
+ - \`GET /api/auth/magic-link/verify?token=xxx\` - Verificar token
1465
+ `;
1466
+ }
1467
+ if (config.auth.googleOAuth) {
1468
+ content += `
1469
+ **Google OAuth:**
1470
+ - \`GET /api/auth/google\` - Redireciona para login Google
1471
+ - \`GET /api/auth/google/callback\` - Callback (configurar no Google Console)
1472
+ `;
1473
+ }
1474
+ }
1475
+ if (config.hasAnyIntegration) {
1476
+ content += `
1477
+ ## Integracoes
1478
+ `;
1479
+ if (config.integrations.viacep) {
1480
+ content += `
1481
+ ### ViaCEP
1482
+ Servico em \`apps/api/src/integrations/viacep/\`
1483
+ \`\`\`typescript
1484
+ import { fetchAddress } from './integrations/viacep/viacep.service';
1485
+ const endereco = await fetchAddress('01001000');
1486
+ \`\`\`
1487
+ `;
1488
+ }
1489
+ if (config.integrations.stripe) {
1490
+ content += `
1491
+ ### Stripe
1492
+ Configurar \`STRIPE_SECRET_KEY\` e \`STRIPE_WEBHOOK_SECRET\` no .env
1493
+ \`\`\`typescript
1494
+ import { createCheckoutSession } from './integrations/stripe/stripe.service';
1495
+ \`\`\`
1496
+ `;
1497
+ }
1498
+ if (config.integrations.mercadoPago) {
1499
+ content += `
1500
+ ### Mercado Pago
1501
+ Configurar \`MERCADOPAGO_ACCESS_TOKEN\` no .env
1502
+ \`\`\`typescript
1503
+ import { createPreference } from './integrations/mercado-pago/mercado-pago.service';
1504
+ \`\`\`
1505
+ `;
1506
+ }
1507
+ if (config.integrations.whatsapp) {
1508
+ content += `
1509
+ ### WhatsApp
1510
+ Configurar \`WHATSAPP_API_TOKEN\` e \`WHATSAPP_PHONE_ID\` no .env
1511
+ \`\`\`typescript
1512
+ import { sendMessage } from './integrations/whatsapp/whatsapp.service';
1513
+ await sendMessage('5511999999999', 'Ol\xE1!');
1514
+ \`\`\`
1515
+ `;
1516
+ }
1517
+ }
1518
+ content += `
1519
+ ## Variaveis de Ambiente
1520
+
1521
+ Edite \`apps/api/.env\` com suas credenciais:
1522
+
1523
+ | Variavel | Descricao |
1524
+ |----------|----------|
1525
+ | \`PORT\` | Porta da API (default: 3001) |
1526
+ `;
1527
+ if (config.database === "postgresql" || config.database === "mysql") {
1528
+ content += `| \`DATABASE_URL\` | URL de conexao do banco |
1529
+ `;
1530
+ }
1531
+ if (config.database === "mongodb") {
1532
+ content += `| \`MONGODB_URI\` | URL de conexao do MongoDB |
1533
+ `;
1534
+ }
1535
+ if (config.auth.jwt) {
1536
+ content += `| \`JWT_SECRET\` | **Alterar em producao!** Secret para tokens |
1537
+ `;
1538
+ }
1539
+ if (config.auth.googleOAuth) {
1540
+ content += `| \`GOOGLE_CLIENT_ID\` | ID do app no Google Console |
1541
+ | \`GOOGLE_CLIENT_SECRET\` | Secret do app no Google Console |
1542
+ `;
1543
+ }
1544
+ if (config.redis) {
1545
+ content += `| \`REDIS_HOST\` | Host do Redis (default: localhost) |
1546
+ `;
1547
+ }
1548
+ if (config.smtp) {
1549
+ content += `| \`SMTP_HOST\`, \`SMTP_USER\`, \`SMTP_PASS\` | Credenciais SMTP |
1550
+ `;
1551
+ }
1552
+ if (config.minio) {
1553
+ content += `| \`MINIO_ACCESS_KEY\`, \`MINIO_SECRET_KEY\` | Credenciais MinIO |
1554
+ `;
1555
+ }
1556
+ if (config.integrations.stripe) {
1557
+ content += `| \`STRIPE_SECRET_KEY\` | Chave secreta do Stripe |
1558
+ `;
1559
+ }
1560
+ if (config.integrations.mercadoPago) {
1561
+ content += `| \`MERCADOPAGO_ACCESS_TOKEN\` | Token do Mercado Pago |
1562
+ `;
1563
+ }
1564
+ content += `
1565
+ ## Scripts
1566
+
1567
+ | Comando | Descricao |
1568
+ |---------|----------|
1569
+ | \`${runCmd} dev\` | Inicia todos os apps |
1570
+ | \`${runCmd} build\` | Build de producao |
1571
+ | \`${runCmd} dev:api\` | Inicia apenas a API |
1572
+ | \`${runCmd} dev:web\` | Inicia apenas o frontend |
1573
+ ${config.usePrisma ? `| \`cd apps/api && npx prisma studio\` | Admin visual do banco |
1574
+ | \`cd apps/api && npx prisma db push\` | Sync schema com banco |` : ""}
1575
+ ${config.pm2 ? `| \`pm2 start ecosystem.config.cjs\` | Deploy com zero-downtime |` : ""}
1576
+ ${hasDocker ? `| \`docker compose up -d\` | Subir infraestrutura |` : ""}
1577
+
1578
+ ## Liz - AI Agent
1579
+
1580
+ Este projeto vem com a **Liz**, uma assistente de IA configurada para conhecer a arquitetura do projeto.
1581
+
1582
+ - **Claude Code:** Liz carrega automaticamente via \`CLAUDE.md\`
1583
+ - **Cursor:** Regras em \`.cursor/rules/liz.mdc\`
1584
+ - **Outros:** Consulte \`AGENTS.md\` para contexto completo
1585
+
1586
+ ---
1587
+
1588
+ Gerado com [PlazerCLI](https://github.com/pablocarss/plazercli)
1589
+ `;
1590
+ await writeFile(path9.join(projectDir, "README.md"), content);
1591
+ }
1592
+
1593
+ // src/generators/wiring.ts
1594
+ import path10 from "path";
1595
+ async function generateWiring(config, apiDir) {
1596
+ switch (config.backend) {
1597
+ case "nestjs":
1598
+ await wireNestjs(config, apiDir);
1599
+ break;
1600
+ case "express":
1601
+ await wireExpress(config, apiDir);
1602
+ break;
1603
+ case "fastify":
1604
+ await wireFastify(config, apiDir);
1605
+ break;
1606
+ }
1607
+ }
1608
+ async function wireNestjs(config, apiDir) {
1609
+ const imports = [];
1610
+ const modules = [];
1611
+ imports.push(`import { Module } from '@nestjs/common';`);
1612
+ imports.push(`import { ConfigModule } from '@nestjs/config';`);
1613
+ imports.push(`import { AppController } from './app.controller';`);
1614
+ imports.push(`import { AppService } from './app.service';`);
1615
+ modules.push(`ConfigModule.forRoot({ isGlobal: true })`);
1616
+ if (config.usePrisma) {
1617
+ imports.push(`import { PrismaModule } from './database/prisma.module';`);
1618
+ modules.push("PrismaModule");
1619
+ }
1620
+ if (config.redis) {
1621
+ imports.push(`import { RedisModule } from './infra/redis/redis.module';`);
1622
+ modules.push("RedisModule");
1623
+ }
1624
+ if (config.smtp) {
1625
+ imports.push(`import { EmailModule } from './infra/email/email.module';`);
1626
+ modules.push("EmailModule");
1627
+ }
1628
+ if (config.minio) {
1629
+ imports.push(`import { StorageModule } from './infra/storage/storage.module';`);
1630
+ modules.push("StorageModule");
1631
+ }
1632
+ if (config.auth.jwt) {
1633
+ imports.push(`import { AuthModule } from './auth/auth.module';`);
1634
+ modules.push("AuthModule");
1635
+ }
1636
+ if (config.multiTenant) {
1637
+ imports.push(`import { TenantModule } from './tenant/tenant.module';`);
1638
+ modules.push("TenantModule");
1639
+ }
1640
+ if (config.queue === "bullmq") {
1641
+ imports.push(`import { QueueModule } from './queue/queue.module';`);
1642
+ modules.push("QueueModule");
1643
+ }
1644
+ if (config.queue === "rabbitmq") {
1645
+ imports.push(`import { RabbitMQModule } from './queue/rabbitmq.config';`);
1646
+ modules.push("RabbitMQModule");
1647
+ }
1648
+ const content = `${imports.join("\n")}
1649
+
1650
+ @Module({
1651
+ imports: [
1652
+ ${modules.join(",\n ")},
1653
+ ],
1654
+ controllers: [AppController],
1655
+ providers: [AppService],
1656
+ })
1657
+ export class AppModule {}
1658
+ `;
1659
+ await writeFile(path10.join(apiDir, "src", "app.module.ts"), content);
1660
+ }
1661
+ async function wireExpress(config, apiDir) {
1662
+ const imports = [];
1663
+ const setupLines = [];
1664
+ const routeLines = [];
1665
+ imports.push(`import express from 'express';`);
1666
+ imports.push(`import cors from 'cors';`);
1667
+ imports.push(`import helmet from 'helmet';`);
1668
+ imports.push(`import { config } from './config/index.js';`);
1669
+ imports.push(`import { router } from './routes/index.js';`);
1670
+ imports.push(`import { errorHandler } from './middlewares/errorHandler.js';`);
1671
+ if (config.useMongoose) {
1672
+ imports.push(`import { connectDB } from './database/connection.js';`);
1673
+ setupLines.push(` // Conectar ao MongoDB`);
1674
+ setupLines.push(` await connectDB();`);
1675
+ }
1676
+ if (config.multiTenant) {
1677
+ imports.push(`import { tenantMiddleware } from './tenant/tenant.middleware.js';`);
1678
+ }
1679
+ if (config.auth.jwt) {
1680
+ imports.push(`import { authRouter } from './auth/auth.routes.js';`);
1681
+ }
1682
+ if (config.auth.magicLink) {
1683
+ imports.push(`import { magicLinkRouter } from './auth/magic-link.routes.js';`);
1684
+ }
1685
+ if (config.auth.googleOAuth) {
1686
+ imports.push(`import { googleRouter } from './auth/google.routes.js';`);
1687
+ }
1688
+ if (config.queue === "rabbitmq") {
1689
+ imports.push(`import { connectRabbitMQ } from './queue/rabbitmq.js';`);
1690
+ setupLines.push(` // Conectar ao RabbitMQ`);
1691
+ setupLines.push(` await connectRabbitMQ();`);
1692
+ }
1693
+ if (config.minio) {
1694
+ imports.push(`import { initStorage } from './infra/storage/storage.js';`);
1695
+ setupLines.push(` // Inicializar MinIO storage`);
1696
+ setupLines.push(` await initStorage();`);
1697
+ }
1698
+ if (config.multiTenant) {
1699
+ routeLines.push(`app.use(tenantMiddleware);`);
1700
+ }
1701
+ if (config.auth.jwt) {
1702
+ routeLines.push(`app.use('/api/auth', authRouter);`);
1703
+ }
1704
+ if (config.auth.magicLink) {
1705
+ routeLines.push(`app.use('/api/auth/magic-link', magicLinkRouter);`);
1706
+ }
1707
+ if (config.auth.googleOAuth) {
1708
+ routeLines.push(`app.use('/api/auth/google', googleRouter);`);
1709
+ }
1710
+ const appContent = `${imports.join("\n")}
1711
+
1712
+ const app = express();
1713
+
1714
+ app.use(helmet());
1715
+ app.use(cors({ origin: config.corsOrigin }));
1716
+ app.use(express.json());
1717
+
1718
+ ${routeLines.length > 0 ? "// Rotas auto-configuradas\n" + routeLines.join("\n") + "\n" : ""}
1719
+ app.use('/api', router);
1720
+
1721
+ app.use(errorHandler);
1722
+
1723
+ export { app };
1724
+ `;
1725
+ const indexContent = `import { app } from './app.js';
1726
+ import { config } from './config/index.js';
1727
+
1728
+ async function start() {
1729
+ ${setupLines.length > 0 ? setupLines.join("\n") + "\n" : ""}
1730
+ app.listen(config.port, () => {
1731
+ console.log(\`\u{1F680} API rodando em http://localhost:\${config.port}\`);
1732
+ });
1733
+ }
1734
+
1735
+ start().catch((err) => {
1736
+ console.error('Erro ao iniciar:', err);
1737
+ process.exit(1);
1738
+ });
1739
+ `;
1740
+ await writeFile(path10.join(apiDir, "src", "app.ts"), appContent);
1741
+ await writeFile(path10.join(apiDir, "src", "index.ts"), indexContent);
1742
+ }
1743
+ async function wireFastify(config, apiDir) {
1744
+ const imports = [];
1745
+ const pluginLines = [];
1746
+ const setupLines = [];
1747
+ imports.push(`import Fastify from 'fastify';`);
1748
+ imports.push(`import cors from '@fastify/cors';`);
1749
+ imports.push(`import helmet from '@fastify/helmet';`);
1750
+ imports.push(`import { config } from './config/index.js';`);
1751
+ imports.push(`import { registerRoutes } from './routes/index.js';`);
1752
+ if (config.useMongoose) {
1753
+ imports.push(`import { connectDB } from './database/connection.js';`);
1754
+ setupLines.push(` await connectDB();`);
1755
+ }
1756
+ if (config.multiTenant) {
1757
+ imports.push(`import tenantPlugin from './tenant/tenant.plugin.js';`);
1758
+ pluginLines.push(` await app.register(tenantPlugin);`);
1759
+ }
1760
+ if (config.auth.jwt) {
1761
+ imports.push(`import { authRoutes } from './auth/auth.routes.js';`);
1762
+ pluginLines.push(` await app.register(authRoutes);`);
1763
+ }
1764
+ if (config.auth.magicLink) {
1765
+ imports.push(`import { magicLinkRoutes } from './auth/magic-link.routes.js';`);
1766
+ pluginLines.push(` await app.register(magicLinkRoutes);`);
1767
+ }
1768
+ if (config.auth.googleOAuth) {
1769
+ imports.push(`import { googleAuthRoutes } from './auth/google.routes.js';`);
1770
+ pluginLines.push(` await app.register(googleAuthRoutes);`);
1771
+ }
1772
+ if (config.queue === "rabbitmq") {
1773
+ imports.push(`import { connectRabbitMQ } from './queue/rabbitmq.js';`);
1774
+ setupLines.push(` await connectRabbitMQ();`);
1775
+ }
1776
+ if (config.minio) {
1777
+ imports.push(`import { initStorage } from './infra/storage/storage.js';`);
1778
+ setupLines.push(` await initStorage();`);
1779
+ }
1780
+ const appContent = `${imports.join("\n")}
1781
+
1782
+ export async function buildApp() {
1783
+ const app = Fastify({
1784
+ logger: config.nodeEnv === 'development',
1785
+ });
1786
+
1787
+ await app.register(helmet);
1788
+ await app.register(cors, { origin: config.corsOrigin });
1789
+
1790
+ ${pluginLines.length > 0 ? " // Plugins auto-configurados\n" + pluginLines.join("\n") + "\n" : ""}
1791
+ registerRoutes(app);
1792
+
1793
+ return app;
1794
+ }
1795
+ `;
1796
+ const indexContent = `import { buildApp } from './app.js';
1797
+ import { config } from './config/index.js';
1798
+
1799
+ async function start() {
1800
+ ${setupLines.length > 0 ? setupLines.join("\n") + "\n" : ""}
1801
+ const app = await buildApp();
1802
+
1803
+ await app.listen({ port: config.port, host: config.host });
1804
+ console.log(\`\u{1F680} API rodando em http://localhost:\${config.port}\`);
1805
+ }
1806
+
1807
+ start().catch((err) => {
1808
+ console.error('Erro ao iniciar:', err);
1809
+ process.exit(1);
1810
+ });
1811
+ `;
1812
+ await writeFile(path10.join(apiDir, "src", "app.ts"), appContent);
1813
+ await writeFile(path10.join(apiDir, "src", "index.ts"), indexContent);
1814
+ }
1815
+
1816
+ // src/generators/liz.ts
1817
+ import path11 from "path";
1818
+ async function generateLiz(config, projectDir) {
1819
+ await generateAgentsMd(config, projectDir);
1820
+ await generateCursorRules(config, projectDir);
1821
+ await generateClaudeMd(config, projectDir);
1822
+ }
1823
+ async function generateAgentsMd(config, projectDir) {
1824
+ const authMethods = [
1825
+ config.auth.jwt && "JWT",
1826
+ config.auth.magicLink && "Magic Link",
1827
+ config.auth.googleOAuth && "Google OAuth"
1828
+ ].filter(Boolean);
1829
+ const integrations = [
1830
+ config.integrations.viacep && "ViaCEP",
1831
+ config.integrations.whatsapp && "WhatsApp",
1832
+ config.integrations.stripe && "Stripe",
1833
+ config.integrations.mercadoPago && "Mercado Pago",
1834
+ config.integrations.abacatePay && "AbacatePay"
1835
+ ].filter(Boolean);
1836
+ const content = `# Liz - AI Development Agent
1837
+
1838
+ ## Identidade
1839
+
1840
+ Eu sou a **Liz**, a assistente de desenvolvimento do projeto **${config.projectName}**. Fui configurada pelo PlazerCLI para conhecer profundamente a arquitetura deste projeto e ajudar no desenvolvimento.
1841
+
1842
+ ## Stack do Projeto
1843
+
1844
+ - **Estrutura:** Monorepo com workspaces
1845
+ - **Frontend:** ${config.frontend} (apps/web/)
1846
+ - **Backend:** ${config.backend} (apps/api/)
1847
+ - **Runtime:** ${config.runtime}
1848
+ ${config.database !== "none" ? `- **Banco de dados:** ${config.database} ${config.usePrisma ? "(Prisma ORM)" : "(Mongoose ODM)"}` : ""}
1849
+ ${authMethods.length > 0 ? `- **Autentica\xE7\xE3o:** ${authMethods.join(", ")}` : ""}
1850
+ ${config.redis ? "- **Cache:** Redis (ioredis)" : ""}
1851
+ ${config.queue !== "none" ? `- **Filas:** ${config.queue}` : ""}
1852
+ ${config.smtp ? "- **Email:** Nodemailer (SMTP)" : ""}
1853
+ ${config.minio ? "- **Storage:** MinIO (S3-compatible)" : ""}
1854
+ ${config.multiTenant ? "- **Multi-tenant:** Sim (isolamento por header/subdomain)" : ""}
1855
+ ${config.pm2 ? "- **Deploy:** PM2 (cluster mode, zero-downtime)" : ""}
1856
+ ${integrations.length > 0 ? `- **Integra\xE7\xF5es:** ${integrations.join(", ")}` : ""}
1857
+
1858
+ ## Estrutura de Diret\xF3rios
1859
+
1860
+ \`\`\`
1861
+ ${config.projectName}/
1862
+ \u251C\u2500\u2500 apps/
1863
+ \u2502 \u251C\u2500\u2500 api/ # Backend (${config.backend})
1864
+ \u2502 \u2502 \u251C\u2500\u2500 src/
1865
+ ${config.usePrisma ? "\u2502 \u2502 \u2502 \u251C\u2500\u2500 database/ # Prisma service e conex\xE3o\n" : ""}${config.useMongoose ? "\u2502 \u2502 \u2502 \u251C\u2500\u2500 database/ # Mongoose models e conex\xE3o\n" : ""}${config.hasAnyAuth ? "\u2502 \u2502 \u2502 \u251C\u2500\u2500 auth/ # Autentica\xE7\xE3o e guards\n" : ""}${config.redis ? "\u2502 \u2502 \u2502 \u251C\u2500\u2500 infra/redis/ # Redis service\n" : ""}${config.smtp ? "\u2502 \u2502 \u2502 \u251C\u2500\u2500 infra/email/ # Email service e templates\n" : ""}${config.minio ? "\u2502 \u2502 \u2502 \u251C\u2500\u2500 infra/storage/ # MinIO storage service\n" : ""}${config.hasAnyQueue ? "\u2502 \u2502 \u2502 \u251C\u2500\u2500 queue/ # Workers e processadores\n" : ""}${config.hasAnyIntegration ? "\u2502 \u2502 \u2502 \u251C\u2500\u2500 integrations/ # Integra\xE7\xF5es externas\n" : ""}${config.multiTenant ? "\u2502 \u2502 \u2502 \u251C\u2500\u2500 tenant/ # Multi-tenant middleware\n" : ""}\u2502 \u2502 \u2502 \u251C\u2500\u2500 routes/ # Rotas da API
1866
+ \u2502 \u2502 \u2502 \u251C\u2500\u2500 config/ # Configura\xE7\xF5es
1867
+ \u2502 \u2502 \u2502 \u2514\u2500\u2500 middlewares/ # Middlewares
1868
+ ${config.usePrisma ? "\u2502 \u2502 \u2514\u2500\u2500 prisma/ # Schema do banco\n" : ""}\u2502 \u2502 \u2514\u2500\u2500 .env # Vari\xE1veis de ambiente
1869
+ \u2502 \u2514\u2500\u2500 web/ # Frontend (${config.frontend})
1870
+ \u2502 \u2514\u2500\u2500 src/
1871
+ \u251C\u2500\u2500 docker-compose.yml
1872
+ \u251C\u2500\u2500 .env.example
1873
+ \u2514\u2500\u2500 README.md
1874
+ \`\`\`
1875
+
1876
+ ## Conven\xE7\xF5es
1877
+
1878
+ ### C\xF3digo
1879
+ - TypeScript strict mode
1880
+ - ESM modules (import/export)
1881
+ - Async/await para opera\xE7\xF5es ass\xEDncronas
1882
+ - Nomes em ingl\xEAs para c\xF3digo, coment\xE1rios podem ser em portugu\xEAs
1883
+
1884
+ ### API
1885
+ - Prefixo \`/api\` em todas as rotas
1886
+ - Health check em \`GET /api/health\`
1887
+ ${config.hasAnyAuth ? `- Rotas de auth em \`/api/auth/*\`` : ""}
1888
+ - Respostas JSON com status HTTP corretos
1889
+
1890
+ ### Banco de Dados
1891
+ ${config.usePrisma ? `- Prisma como ORM
1892
+ - Schema em \`apps/api/prisma/schema.prisma\`
1893
+ - Rodar \`npx prisma db push\` ap\xF3s alterar schema
1894
+ - Rodar \`npx prisma generate\` ap\xF3s alterar models` : ""}
1895
+ ${config.useMongoose ? `- Mongoose como ODM
1896
+ - Models em \`apps/api/src/database/models/\`
1897
+ - Conex\xE3o em \`apps/api/src/database/connection.ts\`` : ""}
1898
+
1899
+ ## Como me usar
1900
+
1901
+ Ao desenvolver, me pe\xE7a para:
1902
+ - Criar novas features seguindo a arquitetura existente
1903
+ - Criar novos endpoints/rotas
1904
+ - Adicionar models ao banco de dados
1905
+ - Criar novos workers/jobs
1906
+ - Integrar novos servi\xE7os
1907
+ - Debugar erros com contexto do projeto
1908
+ - Escrever testes
1909
+
1910
+ Eu conhe\xE7o toda a stack e vou seguir as conven\xE7\xF5es do projeto.
1911
+
1912
+ ---
1913
+ *Configurada por PlazerCLI - https://github.com/pablocarss/plazercli*
1914
+ `;
1915
+ await writeFile(path11.join(projectDir, "AGENTS.md"), content);
1916
+ }
1917
+ async function generateCursorRules(config, projectDir) {
1918
+ await ensureDir(path11.join(projectDir, ".cursor", "rules"));
1919
+ const content = `# Liz - PlazerCLI AI Agent Rules
1920
+
1921
+ You are Liz, the AI development assistant for the "${config.projectName}" project.
1922
+
1923
+ ## Project Context
1924
+ - Monorepo with apps/api (${config.backend}) and apps/web (${config.frontend})
1925
+ - Runtime: ${config.runtime}
1926
+ ${config.database !== "none" ? `- Database: ${config.database} ${config.usePrisma ? "with Prisma ORM" : "with Mongoose ODM"}` : ""}
1927
+ ${config.hasAnyAuth ? "- Authentication configured and wired" : ""}
1928
+ ${config.redis ? "- Redis for caching" : ""}
1929
+ ${config.queue !== "none" ? `- Queue system: ${config.queue}` : ""}
1930
+
1931
+ ## Rules
1932
+ - Always use TypeScript with strict mode
1933
+ - Follow existing patterns in the codebase
1934
+ - Use ESM imports (import/export, .js extensions)
1935
+ - API routes must be prefixed with /api
1936
+ - Use the existing database service/ORM for all DB operations
1937
+ ${config.usePrisma ? "- After schema changes, remind to run: npx prisma db push" : ""}
1938
+ ${config.backend === "nestjs" ? "- Follow NestJS patterns: modules, controllers, services, guards" : ""}
1939
+ ${config.backend === "express" ? "- Use Express Router for new route groups" : ""}
1940
+ ${config.backend === "fastify" ? "- Use Fastify plugins for new functionality" : ""}
1941
+ - Write clean, minimal code - no over-engineering
1942
+ - Handle errors properly with appropriate HTTP status codes
1943
+ `;
1944
+ await writeFile(path11.join(projectDir, ".cursor", "rules", "liz.mdc"), content);
1945
+ }
1946
+ async function generateClaudeMd(config, projectDir) {
1947
+ const content = `# CLAUDE.md - Liz Agent Configuration
1948
+
1949
+ You are **Liz**, the AI development assistant for **${config.projectName}**.
1950
+
1951
+ ## Project Overview
1952
+ This is a fullstack monorepo generated by PlazerCLI.
1953
+
1954
+ - \`apps/api/\` - Backend (${config.backend})
1955
+ - \`apps/web/\` - Frontend (${config.frontend})
1956
+ - Runtime: ${config.runtime}
1957
+ ${config.database !== "none" ? `- Database: ${config.database}` : ""}
1958
+
1959
+ ## Key Files
1960
+ ${config.backend === "nestjs" ? "- `apps/api/src/app.module.ts` - Main module (all features auto-imported)" : ""}
1961
+ ${config.backend === "express" ? "- `apps/api/src/app.ts` - Express app (all routes auto-registered)" : ""}
1962
+ ${config.backend === "fastify" ? "- `apps/api/src/app.ts` - Fastify app (all plugins auto-registered)" : ""}
1963
+ ${config.usePrisma ? "- `apps/api/prisma/schema.prisma` - Database schema" : ""}
1964
+ - \`apps/api/.env\` - Environment variables
1965
+ - \`docker-compose.yml\` - Infrastructure services
1966
+
1967
+ ## Commands
1968
+ - \`${config.runtime} run dev\` - Start all apps
1969
+ - \`${config.runtime} run dev:api\` - Start only API
1970
+ - \`${config.runtime} run dev:web\` - Start only frontend
1971
+ ${config.usePrisma ? "- `cd apps/api && npx prisma db push` - Push schema to DB\n- `cd apps/api && npx prisma studio` - Open DB admin" : ""}
1972
+ - \`docker compose up -d\` - Start infrastructure
1973
+
1974
+ ## Conventions
1975
+ - TypeScript strict, ESM modules
1976
+ - API prefix: /api
1977
+ - Follow existing patterns when creating new features
1978
+ `;
1979
+ await writeFile(path11.join(projectDir, "CLAUDE.md"), content);
1980
+ }
1981
+
1982
+ // src/installers/database/prisma.ts
1983
+ import path12 from "path";
1984
+ var prismaInstaller = async (opts) => {
1985
+ const { config, apiDir, envEntries, dependencies, devDependencies, scripts } = opts;
1986
+ const provider = config.database === "postgresql" ? "postgresql" : "mysql";
1987
+ const dbName = config.projectName.replace(/-/g, "_");
1988
+ await ensureDir(path12.join(apiDir, "prisma"));
1989
+ await ensureDir(path12.join(apiDir, "src", "database"));
1990
+ let schema = `generator client {
1991
+ provider = "prisma-client-js"
1992
+ }
1993
+
1994
+ datasource db {
1995
+ provider = "${provider}"
1996
+ url = env("DATABASE_URL")
1997
+ }
1998
+
1999
+ model User {
2000
+ id String @id @default(cuid())
2001
+ email String @unique
2002
+ name String?
2003
+ password String?
2004
+ createdAt DateTime @default(now()) @map("created_at")
2005
+ updatedAt DateTime @updatedAt @map("updated_at")
2006
+ `;
2007
+ if (config.multiTenant) {
2008
+ schema += ` tenantId String @map("tenant_id")
2009
+ tenant Tenant @relation(fields: [tenantId], references: [id])
2010
+ `;
2011
+ }
2012
+ schema += `
2013
+ @@map("users")
2014
+ }
2015
+ `;
2016
+ if (config.multiTenant) {
2017
+ schema += `
2018
+ model Tenant {
2019
+ id String @id @default(cuid())
2020
+ name String
2021
+ slug String @unique
2022
+ users User[]
2023
+ createdAt DateTime @default(now()) @map("created_at")
2024
+ updatedAt DateTime @updatedAt @map("updated_at")
2025
+
2026
+ @@map("tenants")
2027
+ }
2028
+ `;
2029
+ }
2030
+ await writeFile(path12.join(apiDir, "prisma", "schema.prisma"), schema);
2031
+ if (config.backend === "nestjs") {
2032
+ await writeFile(path12.join(apiDir, "src", "database", "prisma.service.ts"), `import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
2033
+ import { PrismaClient } from '@prisma/client';
2034
+
2035
+ @Injectable()
2036
+ export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
2037
+ async onModuleInit() {
2038
+ await this.$connect();
2039
+ }
2040
+
2041
+ async onModuleDestroy() {
2042
+ await this.$disconnect();
2043
+ }
2044
+ }
2045
+ `);
2046
+ await writeFile(path12.join(apiDir, "src", "database", "prisma.module.ts"), `import { Global, Module } from '@nestjs/common';
2047
+ import { PrismaService } from './prisma.service';
2048
+
2049
+ @Global()
2050
+ @Module({
2051
+ providers: [PrismaService],
2052
+ exports: [PrismaService],
2053
+ })
2054
+ export class PrismaModule {}
2055
+ `);
2056
+ } else {
2057
+ await writeFile(path12.join(apiDir, "src", "database", "prisma.ts"), `import { PrismaClient } from '@prisma/client';
2058
+
2059
+ const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
2060
+
2061
+ export const prisma = globalForPrisma.prisma || new PrismaClient();
2062
+
2063
+ if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
2064
+
2065
+ export default prisma;
2066
+ `);
2067
+ }
2068
+ dependencies["@prisma/client"] = dependencyVersionMap["@prisma/client"];
2069
+ devDependencies["prisma"] = dependencyVersionMap.prisma;
2070
+ scripts["db:push"] = "prisma db push";
2071
+ scripts["db:migrate"] = "prisma migrate dev";
2072
+ scripts["db:studio"] = "prisma studio";
2073
+ scripts["db:seed"] = "prisma db seed";
2074
+ scripts["db:generate"] = "prisma generate";
2075
+ };
2076
+
2077
+ // src/installers/database/mongoose.ts
2078
+ import path13 from "path";
2079
+ var mongooseInstaller = async (opts) => {
2080
+ const { config, apiDir, dependencies } = opts;
2081
+ await ensureDir(path13.join(apiDir, "src", "database"));
2082
+ await ensureDir(path13.join(apiDir, "src", "database", "models"));
2083
+ if (config.multiTenant) {
2084
+ await writeFile(path13.join(apiDir, "src", "database", "connection.ts"), `import mongoose from 'mongoose';
2085
+
2086
+ const connections = new Map<string, mongoose.Connection>();
2087
+
2088
+ export async function connectDB(): Promise<mongoose.Connection> {
2089
+ const uri = process.env.MONGODB_URI || 'mongodb://localhost:27017/${config.projectName.replace(/-/g, "_")}';
2090
+ const conn = await mongoose.connect(uri);
2091
+ console.log('MongoDB conectado');
2092
+ return conn.connection;
2093
+ }
2094
+
2095
+ export function getTenantConnection(tenantId: string): mongoose.Connection {
2096
+ if (connections.has(tenantId)) {
2097
+ return connections.get(tenantId)!;
2098
+ }
2099
+
2100
+ const uri = process.env.MONGODB_URI || 'mongodb://localhost:27017';
2101
+ const conn = mongoose.createConnection(\`\${uri}/tenant_\${tenantId}\`);
2102
+ connections.set(tenantId, conn);
2103
+ return conn;
2104
+ }
2105
+
2106
+ export default { connectDB, getTenantConnection };
2107
+ `);
2108
+ } else {
2109
+ await writeFile(path13.join(apiDir, "src", "database", "connection.ts"), `import mongoose from 'mongoose';
2110
+
2111
+ export async function connectDB(): Promise<void> {
2112
+ const uri = process.env.MONGODB_URI || 'mongodb://localhost:27017/${config.projectName.replace(/-/g, "_")}';
2113
+ await mongoose.connect(uri);
2114
+ console.log('MongoDB conectado');
2115
+ }
2116
+
2117
+ export default { connectDB };
2118
+ `);
2119
+ }
2120
+ await writeFile(path13.join(apiDir, "src", "database", "models", "user.model.ts"), `import mongoose, { Schema, Document } from 'mongoose';
2121
+
2122
+ export interface IUser extends Document {
2123
+ email: string;
2124
+ name?: string;
2125
+ password?: string;
2126
+ ${config.multiTenant ? " tenantId: string;\n" : ""} createdAt: Date;
2127
+ updatedAt: Date;
2128
+ }
2129
+
2130
+ const UserSchema = new Schema<IUser>(
2131
+ {
2132
+ email: { type: String, required: true, unique: true },
2133
+ name: { type: String },
2134
+ password: { type: String },
2135
+ ${config.multiTenant ? " tenantId: { type: String, required: true, index: true },\n" : ""} },
2136
+ { timestamps: true }
2137
+ );
2138
+
2139
+ export const User = mongoose.model<IUser>('User', UserSchema);
2140
+ `);
2141
+ dependencies["mongoose"] = dependencyVersionMap.mongoose;
2142
+ };
2143
+
2144
+ // src/installers/auth/jwt.ts
2145
+ import path14 from "path";
2146
+ var jwtInstaller = async (opts) => {
2147
+ const { config, apiDir, envEntries, dependencies, devDependencies } = opts;
2148
+ await ensureDir(path14.join(apiDir, "src", "auth"));
2149
+ envEntries.push(
2150
+ { key: "JWT_SECRET", value: "change-me-in-production-use-a-strong-secret", category: "Authentication" },
2151
+ { key: "JWT_EXPIRES_IN", value: "7d", category: "Authentication" }
2152
+ );
2153
+ if (config.backend === "nestjs") {
2154
+ await writeFile(path14.join(apiDir, "src", "auth", "jwt.strategy.ts"), `import { Injectable, UnauthorizedException } from '@nestjs/common';
2155
+ import { PassportStrategy } from '@nestjs/passport';
2156
+ import { ExtractJwt, Strategy } from 'passport-jwt';
2157
+ import { ConfigService } from '@nestjs/config';
2158
+
2159
+ @Injectable()
2160
+ export class JwtStrategy extends PassportStrategy(Strategy) {
2161
+ constructor(configService: ConfigService) {
2162
+ super({
2163
+ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
2164
+ ignoreExpiration: false,
2165
+ secretOrKey: configService.get<string>('JWT_SECRET'),
2166
+ });
2167
+ }
2168
+
2169
+ async validate(payload: { sub: string; email: string }) {
2170
+ return { id: payload.sub, email: payload.email };
2171
+ }
2172
+ }
2173
+ `);
2174
+ await writeFile(path14.join(apiDir, "src", "auth", "auth.module.ts"), `import { Module } from '@nestjs/common';
2175
+ import { JwtModule } from '@nestjs/jwt';
2176
+ import { PassportModule } from '@nestjs/passport';
2177
+ import { ConfigModule, ConfigService } from '@nestjs/config';
2178
+ import { AuthService } from './auth.service';
2179
+ import { AuthController } from './auth.controller';
2180
+ import { JwtStrategy } from './jwt.strategy';
2181
+
2182
+ @Module({
2183
+ imports: [
2184
+ PassportModule,
2185
+ JwtModule.registerAsync({
2186
+ imports: [ConfigModule],
2187
+ useFactory: (configService: ConfigService) => ({
2188
+ secret: configService.get<string>('JWT_SECRET'),
2189
+ signOptions: { expiresIn: configService.get<string>('JWT_EXPIRES_IN', '7d') },
2190
+ }),
2191
+ inject: [ConfigService],
2192
+ }),
2193
+ ],
2194
+ controllers: [AuthController],
2195
+ providers: [AuthService, JwtStrategy],
2196
+ exports: [AuthService],
2197
+ })
2198
+ export class AuthModule {}
2199
+ `);
2200
+ await writeFile(path14.join(apiDir, "src", "auth", "auth.service.ts"), `import { Injectable, UnauthorizedException } from '@nestjs/common';
2201
+ import { JwtService } from '@nestjs/jwt';
2202
+ import * as bcrypt from 'bcryptjs';
2203
+
2204
+ @Injectable()
2205
+ export class AuthService {
2206
+ constructor(private jwtService: JwtService) {}
2207
+
2208
+ async hashPassword(password: string): Promise<string> {
2209
+ return bcrypt.hash(password, 10);
2210
+ }
2211
+
2212
+ async comparePassword(password: string, hash: string): Promise<boolean> {
2213
+ return bcrypt.compare(password, hash);
2214
+ }
2215
+
2216
+ async generateToken(payload: { sub: string; email: string }): Promise<string> {
2217
+ return this.jwtService.sign(payload);
2218
+ }
2219
+
2220
+ async validateToken(token: string) {
2221
+ try {
2222
+ return this.jwtService.verify(token);
2223
+ } catch {
2224
+ throw new UnauthorizedException('Token inv\xE1lido');
2225
+ }
2226
+ }
2227
+ }
2228
+ `);
2229
+ await writeFile(path14.join(apiDir, "src", "auth", "auth.controller.ts"), `import { Controller, Post, Body } from '@nestjs/common';
2230
+ import { AuthService } from './auth.service';
2231
+
2232
+ @Controller('auth')
2233
+ export class AuthController {
2234
+ constructor(private readonly authService: AuthService) {}
2235
+
2236
+ @Post('login')
2237
+ async login(@Body() body: { email: string; password: string }) {
2238
+ // TODO: Buscar usuario no banco e validar password
2239
+ const token = await this.authService.generateToken({
2240
+ sub: 'user-id',
2241
+ email: body.email,
2242
+ });
2243
+ return { access_token: token };
2244
+ }
2245
+
2246
+ @Post('register')
2247
+ async register(@Body() body: { email: string; password: string; name?: string }) {
2248
+ const hashedPassword = await this.authService.hashPassword(body.password);
2249
+ // TODO: Criar usuario no banco com hashedPassword
2250
+ const token = await this.authService.generateToken({
2251
+ sub: 'new-user-id',
2252
+ email: body.email,
2253
+ });
2254
+ return { access_token: token };
2255
+ }
2256
+ }
2257
+ `);
2258
+ await writeFile(path14.join(apiDir, "src", "auth", "jwt-auth.guard.ts"), `import { Injectable } from '@nestjs/common';
2259
+ import { AuthGuard } from '@nestjs/passport';
2260
+
2261
+ @Injectable()
2262
+ export class JwtAuthGuard extends AuthGuard('jwt') {}
2263
+ `);
2264
+ dependencies["@nestjs/jwt"] = dependencyVersionMap["@nestjs/jwt"];
2265
+ dependencies["@nestjs/passport"] = dependencyVersionMap["@nestjs/passport"];
2266
+ dependencies["passport"] = dependencyVersionMap.passport;
2267
+ dependencies["passport-jwt"] = dependencyVersionMap["passport-jwt"];
2268
+ devDependencies["@types/passport-jwt"] = dependencyVersionMap["@types/passport-jwt"];
2269
+ } else {
2270
+ await writeFile(path14.join(apiDir, "src", "auth", "jwt.ts"), `import jwt from 'jsonwebtoken';
2271
+
2272
+ const JWT_SECRET = process.env.JWT_SECRET || 'change-me';
2273
+ const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
2274
+
2275
+ export function generateToken(payload: { sub: string; email: string }): string {
2276
+ return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
2277
+ }
2278
+
2279
+ export function verifyToken(token: string) {
2280
+ return jwt.verify(token, JWT_SECRET);
2281
+ }
2282
+ `);
2283
+ await writeFile(path14.join(apiDir, "src", "auth", "hash.ts"), `import bcrypt from 'bcryptjs';
2284
+
2285
+ export async function hashPassword(password: string): Promise<string> {
2286
+ return bcrypt.hash(password, 10);
2287
+ }
2288
+
2289
+ export async function comparePassword(password: string, hash: string): Promise<boolean> {
2290
+ return bcrypt.compare(password, hash);
2291
+ }
2292
+ `);
2293
+ if (config.backend === "express") {
2294
+ await writeFile(path14.join(apiDir, "src", "auth", "auth.middleware.ts"), `import type { Request, Response, NextFunction } from 'express';
2295
+ import { verifyToken } from './jwt.js';
2296
+
2297
+ export function authMiddleware(req: Request, res: Response, next: NextFunction) {
2298
+ const authHeader = req.headers.authorization;
2299
+ if (!authHeader?.startsWith('Bearer ')) {
2300
+ return res.status(401).json({ error: 'Token n\xE3o fornecido' });
2301
+ }
2302
+
2303
+ try {
2304
+ const token = authHeader.split(' ')[1];
2305
+ const payload = verifyToken(token);
2306
+ (req as any).user = payload;
2307
+ next();
2308
+ } catch {
2309
+ return res.status(401).json({ error: 'Token inv\xE1lido' });
2310
+ }
2311
+ }
2312
+ `);
2313
+ await writeFile(path14.join(apiDir, "src", "auth", "auth.routes.ts"), `import { Router } from 'express';
2314
+ import { generateToken } from './jwt.js';
2315
+ import { hashPassword, comparePassword } from './hash.js';
2316
+
2317
+ const authRouter = Router();
2318
+
2319
+ authRouter.post('/login', async (req, res) => {
2320
+ const { email, password } = req.body;
2321
+ // TODO: Buscar usuario no banco e validar password
2322
+ const token = generateToken({ sub: 'user-id', email });
2323
+ res.json({ access_token: token });
2324
+ });
2325
+
2326
+ authRouter.post('/register', async (req, res) => {
2327
+ const { email, password, name } = req.body;
2328
+ const hashedPassword = await hashPassword(password);
2329
+ // TODO: Criar usuario no banco com hashedPassword
2330
+ const token = generateToken({ sub: 'new-user-id', email });
2331
+ res.json({ access_token: token });
2332
+ });
2333
+
2334
+ export { authRouter };
2335
+ `);
2336
+ } else {
2337
+ await writeFile(path14.join(apiDir, "src", "auth", "auth.hook.ts"), `import type { FastifyRequest, FastifyReply } from 'fastify';
2338
+ import { verifyToken } from './jwt.js';
2339
+
2340
+ export async function authHook(request: FastifyRequest, reply: FastifyReply) {
2341
+ const authHeader = request.headers.authorization;
2342
+ if (!authHeader?.startsWith('Bearer ')) {
2343
+ return reply.status(401).send({ error: 'Token n\xE3o fornecido' });
2344
+ }
2345
+
2346
+ try {
2347
+ const token = authHeader.split(' ')[1];
2348
+ const payload = verifyToken(token);
2349
+ (request as any).user = payload;
2350
+ } catch {
2351
+ return reply.status(401).send({ error: 'Token inv\xE1lido' });
2352
+ }
2353
+ }
2354
+ `);
2355
+ await writeFile(path14.join(apiDir, "src", "auth", "auth.routes.ts"), `import type { FastifyInstance } from 'fastify';
2356
+ import { generateToken } from './jwt.js';
2357
+ import { hashPassword } from './hash.js';
2358
+
2359
+ export async function authRoutes(app: FastifyInstance) {
2360
+ app.post('/api/auth/login', async (request) => {
2361
+ const { email, password } = request.body as { email: string; password: string };
2362
+ // TODO: Buscar usuario no banco e validar password
2363
+ const token = generateToken({ sub: 'user-id', email });
2364
+ return { access_token: token };
2365
+ });
2366
+
2367
+ app.post('/api/auth/register', async (request) => {
2368
+ const { email, password, name } = request.body as { email: string; password: string; name?: string };
2369
+ const hashedPassword = await hashPassword(password);
2370
+ // TODO: Criar usuario no banco com hashedPassword
2371
+ const token = generateToken({ sub: 'new-user-id', email });
2372
+ return { access_token: token };
2373
+ });
2374
+ }
2375
+ `);
2376
+ }
2377
+ dependencies["jsonwebtoken"] = dependencyVersionMap.jsonwebtoken;
2378
+ devDependencies["@types/jsonwebtoken"] = dependencyVersionMap["@types/jsonwebtoken"];
2379
+ }
2380
+ dependencies["bcryptjs"] = dependencyVersionMap.bcryptjs;
2381
+ devDependencies["@types/bcryptjs"] = dependencyVersionMap["@types/bcryptjs"];
2382
+ };
2383
+
2384
+ // src/installers/auth/magicLink.ts
2385
+ import path15 from "path";
2386
+ var magicLinkInstaller = async (opts) => {
2387
+ const { config, apiDir, envEntries, dependencies } = opts;
2388
+ await ensureDir(path15.join(apiDir, "src", "auth"));
2389
+ envEntries.push(
2390
+ { key: "MAGIC_LINK_EXPIRY_MINUTES", value: "15", category: "Authentication" },
2391
+ { key: "APP_URL", value: "http://localhost:3000", category: "Authentication", comment: "URL do frontend para o magic link" }
2392
+ );
2393
+ const serviceContent = `import { nanoid } from 'nanoid';
2394
+
2395
+ interface MagicLinkToken {
2396
+ token: string;
2397
+ email: string;
2398
+ expiresAt: Date;
2399
+ }
2400
+
2401
+ // Em producao, armazene no banco de dados ou Redis
2402
+ const tokens = new Map<string, MagicLinkToken>();
2403
+
2404
+ const EXPIRY_MINUTES = Number(process.env.MAGIC_LINK_EXPIRY_MINUTES) || 15;
2405
+ const APP_URL = process.env.APP_URL || 'http://localhost:3000';
2406
+
2407
+ export function generateMagicLink(email: string): { token: string; url: string } {
2408
+ const token = nanoid(32);
2409
+ const expiresAt = new Date(Date.now() + EXPIRY_MINUTES * 60 * 1000);
2410
+
2411
+ tokens.set(token, { token, email, expiresAt });
2412
+
2413
+ const url = \`\${APP_URL}/auth/verify?token=\${token}\`;
2414
+ return { token, url };
2415
+ }
2416
+
2417
+ export function verifyMagicLinkToken(token: string): { valid: boolean; email?: string } {
2418
+ const entry = tokens.get(token);
2419
+ if (!entry) return { valid: false };
2420
+ if (entry.expiresAt < new Date()) {
2421
+ tokens.delete(token);
2422
+ return { valid: false };
2423
+ }
2424
+ tokens.delete(token);
2425
+ return { valid: true, email: entry.email };
2426
+ }
2427
+ `;
2428
+ await writeFile(path15.join(apiDir, "src", "auth", "magic-link.service.ts"), serviceContent);
2429
+ if (config.backend === "nestjs") {
2430
+ await writeFile(path15.join(apiDir, "src", "auth", "magic-link.controller.ts"), `import { Controller, Post, Body, Get, Query, BadRequestException } from '@nestjs/common';
2431
+ import { generateMagicLink, verifyMagicLinkToken } from './magic-link.service';
2432
+
2433
+ @Controller('auth/magic-link')
2434
+ export class MagicLinkController {
2435
+ @Post('send')
2436
+ async sendMagicLink(@Body() body: { email: string }) {
2437
+ const { url } = generateMagicLink(body.email);
2438
+ // TODO: Enviar email com o magic link usando o email service
2439
+ console.log('Magic link:', url);
2440
+ return { message: 'Magic link enviado para o email' };
2441
+ }
2442
+
2443
+ @Get('verify')
2444
+ async verify(@Query('token') token: string) {
2445
+ const result = verifyMagicLinkToken(token);
2446
+ if (!result.valid) {
2447
+ throw new BadRequestException('Token inv\xE1lido ou expirado');
2448
+ }
2449
+ // TODO: Criar sessao/JWT para o usuario
2450
+ return { message: 'Login realizado', email: result.email };
2451
+ }
2452
+ }
2453
+ `);
2454
+ } else if (config.backend === "express") {
2455
+ await writeFile(path15.join(apiDir, "src", "auth", "magic-link.routes.ts"), `import { Router } from 'express';
2456
+ import { generateMagicLink, verifyMagicLinkToken } from './magic-link.service.js';
2457
+
2458
+ const magicLinkRouter = Router();
2459
+
2460
+ magicLinkRouter.post('/send', async (req, res) => {
2461
+ const { email } = req.body;
2462
+ const { url } = generateMagicLink(email);
2463
+ // TODO: Enviar email com o magic link usando o email service
2464
+ console.log('Magic link:', url);
2465
+ res.json({ message: 'Magic link enviado para o email' });
2466
+ });
2467
+
2468
+ magicLinkRouter.get('/verify', async (req, res) => {
2469
+ const token = req.query.token as string;
2470
+ const result = verifyMagicLinkToken(token);
2471
+ if (!result.valid) {
2472
+ return res.status(400).json({ error: 'Token inv\xE1lido ou expirado' });
2473
+ }
2474
+ // TODO: Criar sessao/JWT para o usuario
2475
+ res.json({ message: 'Login realizado', email: result.email });
2476
+ });
2477
+
2478
+ export { magicLinkRouter };
2479
+ `);
2480
+ } else {
2481
+ await writeFile(path15.join(apiDir, "src", "auth", "magic-link.routes.ts"), `import type { FastifyInstance } from 'fastify';
2482
+ import { generateMagicLink, verifyMagicLinkToken } from './magic-link.service.js';
2483
+
2484
+ export async function magicLinkRoutes(app: FastifyInstance) {
2485
+ app.post('/api/auth/magic-link/send', async (request) => {
2486
+ const { email } = request.body as { email: string };
2487
+ const { url } = generateMagicLink(email);
2488
+ // TODO: Enviar email com o magic link usando o email service
2489
+ console.log('Magic link:', url);
2490
+ return { message: 'Magic link enviado para o email' };
2491
+ });
2492
+
2493
+ app.get('/api/auth/magic-link/verify', async (request, reply) => {
2494
+ const { token } = request.query as { token: string };
2495
+ const result = verifyMagicLinkToken(token);
2496
+ if (!result.valid) {
2497
+ return reply.status(400).send({ error: 'Token inv\xE1lido ou expirado' });
2498
+ }
2499
+ // TODO: Criar sessao/JWT para o usuario
2500
+ return { message: 'Login realizado', email: result.email };
2501
+ });
2502
+ }
2503
+ `);
2504
+ }
2505
+ dependencies["nanoid"] = dependencyVersionMap.nanoid;
2506
+ };
2507
+
2508
+ // src/installers/auth/googleOauth.ts
2509
+ import path16 from "path";
2510
+ var googleOauthInstaller = async (opts) => {
2511
+ const { config, apiDir, envEntries, dependencies, devDependencies } = opts;
2512
+ await ensureDir(path16.join(apiDir, "src", "auth"));
2513
+ envEntries.push(
2514
+ { key: "GOOGLE_CLIENT_ID", value: "", category: "Google OAuth", comment: "Obtenha em https://console.cloud.google.com" },
2515
+ { key: "GOOGLE_CLIENT_SECRET", value: "", category: "Google OAuth" },
2516
+ { key: "GOOGLE_CALLBACK_URL", value: "http://localhost:3001/api/auth/google/callback", category: "Google OAuth" }
2517
+ );
2518
+ if (config.backend === "nestjs") {
2519
+ await writeFile(path16.join(apiDir, "src", "auth", "google.strategy.ts"), `import { Injectable } from '@nestjs/common';
2520
+ import { PassportStrategy } from '@nestjs/passport';
2521
+ import { Strategy, VerifyCallback, Profile } from 'passport-google-oauth20';
2522
+ import { ConfigService } from '@nestjs/config';
2523
+
2524
+ @Injectable()
2525
+ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
2526
+ constructor(configService: ConfigService) {
2527
+ super({
2528
+ clientID: configService.get<string>('GOOGLE_CLIENT_ID'),
2529
+ clientSecret: configService.get<string>('GOOGLE_CLIENT_SECRET'),
2530
+ callbackURL: configService.get<string>('GOOGLE_CALLBACK_URL'),
2531
+ scope: ['email', 'profile'],
2532
+ });
2533
+ }
2534
+
2535
+ async validate(
2536
+ accessToken: string,
2537
+ refreshToken: string,
2538
+ profile: Profile,
2539
+ done: VerifyCallback,
2540
+ ) {
2541
+ const user = {
2542
+ email: profile.emails?.[0]?.value,
2543
+ name: profile.displayName,
2544
+ googleId: profile.id,
2545
+ avatar: profile.photos?.[0]?.value,
2546
+ };
2547
+ done(null, user);
2548
+ }
2549
+ }
2550
+ `);
2551
+ await writeFile(path16.join(apiDir, "src", "auth", "google.controller.ts"), `import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common';
2552
+ import { AuthGuard } from '@nestjs/passport';
2553
+ import type { Request, Response } from 'express';
2554
+
2555
+ @Controller('auth/google')
2556
+ export class GoogleController {
2557
+ @Get()
2558
+ @UseGuards(AuthGuard('google'))
2559
+ async googleAuth() {
2560
+ // Redireciona para o Google
2561
+ }
2562
+
2563
+ @Get('callback')
2564
+ @UseGuards(AuthGuard('google'))
2565
+ async googleCallback(@Req() req: Request, @Res() res: Response) {
2566
+ const user = req.user;
2567
+ // TODO: Criar/buscar usuario no banco, gerar JWT
2568
+ console.log('Google user:', user);
2569
+ res.redirect(process.env.APP_URL || 'http://localhost:3000');
2570
+ }
2571
+ }
2572
+ `);
2573
+ dependencies["@nestjs/passport"] = dependencyVersionMap["@nestjs/passport"];
2574
+ dependencies["passport"] = dependencyVersionMap.passport;
2575
+ } else if (config.backend === "express") {
2576
+ await writeFile(path16.join(apiDir, "src", "auth", "google.routes.ts"), `import { Router } from 'express';
2577
+ import passport from 'passport';
2578
+ import { Strategy as GoogleStrategy } from 'passport-google-oauth20';
2579
+
2580
+ passport.use(
2581
+ new GoogleStrategy(
2582
+ {
2583
+ clientID: process.env.GOOGLE_CLIENT_ID || '',
2584
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET || '',
2585
+ callbackURL: process.env.GOOGLE_CALLBACK_URL || 'http://localhost:3001/api/auth/google/callback',
2586
+ },
2587
+ async (accessToken, refreshToken, profile, done) => {
2588
+ const user = {
2589
+ email: profile.emails?.[0]?.value,
2590
+ name: profile.displayName,
2591
+ googleId: profile.id,
2592
+ avatar: profile.photos?.[0]?.value,
2593
+ };
2594
+ // TODO: Criar/buscar usuario no banco
2595
+ done(null, user);
2596
+ }
2597
+ )
2598
+ );
2599
+
2600
+ const googleRouter = Router();
2601
+
2602
+ googleRouter.get('/', passport.authenticate('google', { scope: ['profile', 'email'] }));
2603
+
2604
+ googleRouter.get(
2605
+ '/callback',
2606
+ passport.authenticate('google', { session: false }),
2607
+ (req, res) => {
2608
+ // TODO: Gerar JWT e redirecionar
2609
+ console.log('Google user:', req.user);
2610
+ res.redirect(process.env.APP_URL || 'http://localhost:3000');
2611
+ }
2612
+ );
2613
+
2614
+ export { googleRouter };
2615
+ `);
2616
+ dependencies["passport"] = dependencyVersionMap.passport;
2617
+ } else {
2618
+ await writeFile(path16.join(apiDir, "src", "auth", "google.routes.ts"), `import type { FastifyInstance } from 'fastify';
2619
+
2620
+ // Para Fastify, recomendamos usar @fastify/oauth2
2621
+ // npm install @fastify/oauth2
2622
+ export async function googleAuthRoutes(app: FastifyInstance) {
2623
+ // TODO: Configurar @fastify/oauth2 para Google
2624
+ // Documentacao: https://github.com/fastify/fastify-oauth2
2625
+
2626
+ app.get('/api/auth/google', async (request, reply) => {
2627
+ const clientId = process.env.GOOGLE_CLIENT_ID;
2628
+ const redirectUri = process.env.GOOGLE_CALLBACK_URL;
2629
+ const url = \`https://accounts.google.com/o/oauth2/v2/auth?client_id=\${clientId}&redirect_uri=\${redirectUri}&response_type=code&scope=email profile\`;
2630
+ reply.redirect(url);
2631
+ });
2632
+
2633
+ app.get('/api/auth/google/callback', async (request) => {
2634
+ const { code } = request.query as { code: string };
2635
+ // TODO: Trocar code por tokens, buscar perfil, criar/buscar usuario
2636
+ return { message: 'Google OAuth callback', code };
2637
+ });
2638
+ }
2639
+ `);
2640
+ }
2641
+ dependencies["passport-google-oauth20"] = dependencyVersionMap["passport-google-oauth20"];
2642
+ devDependencies["@types/passport-google-oauth20"] = dependencyVersionMap["@types/passport-google-oauth20"];
2643
+ };
2644
+
2645
+ // src/installers/queue/bullmq.ts
2646
+ import path17 from "path";
2647
+ var bullmqInstaller = async (opts) => {
2648
+ const { config, apiDir, dependencies } = opts;
2649
+ await ensureDir(path17.join(apiDir, "src", "queue"));
2650
+ if (config.backend === "nestjs") {
2651
+ await writeFile(path17.join(apiDir, "src", "queue", "queue.module.ts"), `import { Module } from '@nestjs/common';
2652
+ import { BullModule } from '@nestjs/bullmq';
2653
+ import { ConfigModule, ConfigService } from '@nestjs/config';
2654
+ import { ExampleProcessor } from './example.processor';
2655
+
2656
+ @Module({
2657
+ imports: [
2658
+ BullModule.forRootAsync({
2659
+ imports: [ConfigModule],
2660
+ useFactory: (configService: ConfigService) => ({
2661
+ connection: {
2662
+ host: configService.get('REDIS_HOST', 'localhost'),
2663
+ port: configService.get('REDIS_PORT', 6379),
2664
+ password: configService.get('REDIS_PASSWORD') || undefined,
2665
+ },
2666
+ }),
2667
+ inject: [ConfigService],
2668
+ }),
2669
+ BullModule.registerQueue({ name: 'example' }),
2670
+ ],
2671
+ providers: [ExampleProcessor],
2672
+ exports: [BullModule],
2673
+ })
2674
+ export class QueueModule {}
2675
+ `);
2676
+ await writeFile(path17.join(apiDir, "src", "queue", "example.processor.ts"), `import { Processor, WorkerHost } from '@nestjs/bullmq';
2677
+ import { Job } from 'bullmq';
2678
+
2679
+ @Processor('example')
2680
+ export class ExampleProcessor extends WorkerHost {
2681
+ async process(job: Job<{ message: string }>) {
2682
+ console.log(\`Processando job \${job.id}: \${job.data.message}\`);
2683
+ // TODO: Implemente a logica do job aqui
2684
+ return { success: true };
2685
+ }
2686
+ }
2687
+ `);
2688
+ dependencies["@nestjs/bullmq"] = dependencyVersionMap["@nestjs/bullmq"];
2689
+ } else {
2690
+ await writeFile(path17.join(apiDir, "src", "queue", "queue.config.ts"), `import { Queue, Worker } from 'bullmq';
2691
+
2692
+ const connection = {
2693
+ host: process.env.REDIS_HOST || 'localhost',
2694
+ port: Number(process.env.REDIS_PORT) || 6379,
2695
+ password: process.env.REDIS_PASSWORD || undefined,
2696
+ };
2697
+
2698
+ export function createQueue(name: string) {
2699
+ return new Queue(name, { connection });
2700
+ }
2701
+
2702
+ export function createWorker(
2703
+ name: string,
2704
+ processor: (job: any) => Promise<any>
2705
+ ) {
2706
+ const worker = new Worker(name, processor, { connection });
2707
+
2708
+ worker.on('completed', (job) => {
2709
+ console.log(\`Job \${job.id} concluido\`);
2710
+ });
2711
+
2712
+ worker.on('failed', (job, err) => {
2713
+ console.error(\`Job \${job?.id} falhou:\`, err);
2714
+ });
2715
+
2716
+ return worker;
2717
+ }
2718
+ `);
2719
+ await writeFile(path17.join(apiDir, "src", "queue", "example.worker.ts"), `import { createQueue, createWorker } from './queue.config.js';
2720
+
2721
+ // Criar fila
2722
+ export const exampleQueue = createQueue('example');
2723
+
2724
+ // Criar worker
2725
+ export const exampleWorker = createWorker('example', async (job) => {
2726
+ console.log(\`Processando job \${job.id}: \${job.data.message}\`);
2727
+ // TODO: Implemente a logica do job aqui
2728
+ return { success: true };
2729
+ });
2730
+
2731
+ // Funcao helper para adicionar job na fila
2732
+ export async function addExampleJob(data: { message: string }) {
2733
+ return exampleQueue.add('process', data);
2734
+ }
2735
+ `);
2736
+ }
2737
+ dependencies["bullmq"] = dependencyVersionMap.bullmq;
2738
+ };
2739
+
2740
+ // src/installers/queue/rabbitmq.ts
2741
+ import path18 from "path";
2742
+ var rabbitmqInstaller = async (opts) => {
2743
+ const { config, apiDir, envEntries, dependencies, devDependencies } = opts;
2744
+ await ensureDir(path18.join(apiDir, "src", "queue"));
2745
+ envEntries.push(
2746
+ { key: "RABBITMQ_URL", value: "amqp://guest:guest@localhost:5672", category: "RabbitMQ" }
2747
+ );
2748
+ if (config.backend === "nestjs") {
2749
+ await writeFile(path18.join(apiDir, "src", "queue", "rabbitmq.config.ts"), `import { Module } from '@nestjs/common';
2750
+ import { ClientsModule, Transport } from '@nestjs/microservices';
2751
+
2752
+ @Module({
2753
+ imports: [
2754
+ ClientsModule.register([
2755
+ {
2756
+ name: 'RABBITMQ_SERVICE',
2757
+ transport: Transport.RMQ,
2758
+ options: {
2759
+ urls: [process.env.RABBITMQ_URL || 'amqp://guest:guest@localhost:5672'],
2760
+ queue: 'main_queue',
2761
+ queueOptions: { durable: true },
2762
+ },
2763
+ },
2764
+ ]),
2765
+ ],
2766
+ exports: [ClientsModule],
2767
+ })
2768
+ export class RabbitMQModule {}
2769
+ `);
2770
+ await writeFile(path18.join(apiDir, "src", "queue", "rabbitmq.consumer.ts"), `import { Controller } from '@nestjs/common';
2771
+ import { MessagePattern, Payload } from '@nestjs/microservices';
2772
+
2773
+ @Controller()
2774
+ export class RabbitMQConsumer {
2775
+ @MessagePattern('example_pattern')
2776
+ async handleMessage(@Payload() data: any) {
2777
+ console.log('Mensagem recebida:', data);
2778
+ // TODO: Processar mensagem
2779
+ return { success: true };
2780
+ }
2781
+ }
2782
+ `);
2783
+ dependencies["@nestjs/microservices"] = dependencyVersionMap["@nestjs/common"];
2784
+ } else {
2785
+ await writeFile(path18.join(apiDir, "src", "queue", "rabbitmq.ts"), `import amqplib, { type Channel, type Connection } from 'amqplib';
2786
+
2787
+ let connection: Connection | null = null;
2788
+ let channel: Channel | null = null;
2789
+
2790
+ export async function connectRabbitMQ(): Promise<Channel> {
2791
+ const url = process.env.RABBITMQ_URL || 'amqp://guest:guest@localhost:5672';
2792
+ connection = await amqplib.connect(url);
2793
+ channel = await connection.createChannel();
2794
+ console.log('RabbitMQ conectado');
2795
+ return channel;
2796
+ }
2797
+
2798
+ export async function publishMessage(queue: string, message: unknown): Promise<void> {
2799
+ if (!channel) throw new Error('RabbitMQ nao conectado');
2800
+ await channel.assertQueue(queue, { durable: true });
2801
+ channel.sendToQueue(queue, Buffer.from(JSON.stringify(message)), { persistent: true });
2802
+ }
2803
+
2804
+ export async function consumeMessages(
2805
+ queue: string,
2806
+ handler: (msg: unknown) => Promise<void>
2807
+ ): Promise<void> {
2808
+ if (!channel) throw new Error('RabbitMQ nao conectado');
2809
+ await channel.assertQueue(queue, { durable: true });
2810
+ channel.consume(queue, async (msg) => {
2811
+ if (!msg) return;
2812
+ try {
2813
+ const data = JSON.parse(msg.content.toString());
2814
+ await handler(data);
2815
+ channel!.ack(msg);
2816
+ } catch (err) {
2817
+ console.error('Erro ao processar mensagem:', err);
2818
+ channel!.nack(msg, false, true);
2819
+ }
2820
+ });
2821
+ }
2822
+
2823
+ export async function closeRabbitMQ(): Promise<void> {
2824
+ await channel?.close();
2825
+ await connection?.close();
2826
+ }
2827
+ `);
2828
+ }
2829
+ dependencies["amqplib"] = dependencyVersionMap.amqplib;
2830
+ devDependencies["@types/amqplib"] = dependencyVersionMap["@types/amqplib"];
2831
+ };
2832
+
2833
+ // src/installers/infra/redis.ts
2834
+ import path19 from "path";
2835
+ var redisInstaller = async (opts) => {
2836
+ const { config, apiDir, envEntries, dependencies } = opts;
2837
+ await ensureDir(path19.join(apiDir, "src", "infra", "redis"));
2838
+ envEntries.push(
2839
+ { key: "REDIS_HOST", value: "localhost", category: "Redis" },
2840
+ { key: "REDIS_PORT", value: "6379", category: "Redis" },
2841
+ { key: "REDIS_PASSWORD", value: "", category: "Redis" }
2842
+ );
2843
+ if (config.backend === "nestjs") {
2844
+ await writeFile(path19.join(apiDir, "src", "infra", "redis", "redis.service.ts"), `import { Injectable, OnModuleDestroy } from '@nestjs/common';
2845
+ import { ConfigService } from '@nestjs/config';
2846
+ import Redis from 'ioredis';
2847
+
2848
+ @Injectable()
2849
+ export class RedisService implements OnModuleDestroy {
2850
+ public readonly client: Redis;
2851
+
2852
+ constructor(private configService: ConfigService) {
2853
+ this.client = new Redis({
2854
+ host: this.configService.get('REDIS_HOST', 'localhost'),
2855
+ port: this.configService.get('REDIS_PORT', 6379),
2856
+ password: this.configService.get('REDIS_PASSWORD') || undefined,
2857
+ });
2858
+ }
2859
+
2860
+ async get(key: string): Promise<string | null> {
2861
+ return this.client.get(key);
2862
+ }
2863
+
2864
+ async set(key: string, value: string, ttlSeconds?: number): Promise<void> {
2865
+ if (ttlSeconds) {
2866
+ await this.client.set(key, value, 'EX', ttlSeconds);
2867
+ } else {
2868
+ await this.client.set(key, value);
2869
+ }
2870
+ }
2871
+
2872
+ async del(key: string): Promise<void> {
2873
+ await this.client.del(key);
2874
+ }
2875
+
2876
+ async onModuleDestroy() {
2877
+ await this.client.quit();
2878
+ }
2879
+ }
2880
+ `);
2881
+ await writeFile(path19.join(apiDir, "src", "infra", "redis", "redis.module.ts"), `import { Global, Module } from '@nestjs/common';
2882
+ import { RedisService } from './redis.service';
2883
+
2884
+ @Global()
2885
+ @Module({
2886
+ providers: [RedisService],
2887
+ exports: [RedisService],
2888
+ })
2889
+ export class RedisModule {}
2890
+ `);
2891
+ } else {
2892
+ await writeFile(path19.join(apiDir, "src", "infra", "redis", "redis.ts"), `import Redis from 'ioredis';
2893
+
2894
+ const redis = new Redis({
2895
+ host: process.env.REDIS_HOST || 'localhost',
2896
+ port: Number(process.env.REDIS_PORT) || 6379,
2897
+ password: process.env.REDIS_PASSWORD || undefined,
2898
+ });
2899
+
2900
+ redis.on('connect', () => console.log('Redis conectado'));
2901
+ redis.on('error', (err) => console.error('Redis erro:', err));
2902
+
2903
+ export default redis;
2904
+ `);
2905
+ }
2906
+ dependencies["ioredis"] = dependencyVersionMap.ioredis;
2907
+ };
2908
+
2909
+ // src/installers/infra/smtp.ts
2910
+ import path20 from "path";
2911
+ var smtpInstaller = async (opts) => {
2912
+ const { config, apiDir, envEntries, dependencies, devDependencies } = opts;
2913
+ await ensureDir(path20.join(apiDir, "src", "infra", "email"));
2914
+ await ensureDir(path20.join(apiDir, "src", "infra", "email", "templates"));
2915
+ envEntries.push(
2916
+ { key: "SMTP_HOST", value: "", category: "SMTP", comment: "Ex: smtp.gmail.com" },
2917
+ { key: "SMTP_PORT", value: "587", category: "SMTP" },
2918
+ { key: "SMTP_USER", value: "", category: "SMTP" },
2919
+ { key: "SMTP_PASS", value: "", category: "SMTP" },
2920
+ { key: "SMTP_FROM", value: `noreply@${config.projectName}.com`, category: "SMTP" }
2921
+ );
2922
+ if (config.backend === "nestjs") {
2923
+ await writeFile(path20.join(apiDir, "src", "infra", "email", "email.service.ts"), `import { Injectable } from '@nestjs/common';
2924
+ import { ConfigService } from '@nestjs/config';
2925
+ import * as nodemailer from 'nodemailer';
2926
+
2927
+ @Injectable()
2928
+ export class EmailService {
2929
+ private transporter: nodemailer.Transporter;
2930
+
2931
+ constructor(private configService: ConfigService) {
2932
+ this.transporter = nodemailer.createTransport({
2933
+ host: this.configService.get('SMTP_HOST'),
2934
+ port: this.configService.get('SMTP_PORT', 587),
2935
+ secure: false,
2936
+ auth: {
2937
+ user: this.configService.get('SMTP_USER'),
2938
+ pass: this.configService.get('SMTP_PASS'),
2939
+ },
2940
+ });
2941
+ }
2942
+
2943
+ async sendEmail(to: string, subject: string, html: string): Promise<void> {
2944
+ await this.transporter.sendMail({
2945
+ from: this.configService.get('SMTP_FROM'),
2946
+ to,
2947
+ subject,
2948
+ html,
2949
+ });
2950
+ }
2951
+ }
2952
+ `);
2953
+ await writeFile(path20.join(apiDir, "src", "infra", "email", "email.module.ts"), `import { Global, Module } from '@nestjs/common';
2954
+ import { EmailService } from './email.service';
2955
+
2956
+ @Global()
2957
+ @Module({
2958
+ providers: [EmailService],
2959
+ exports: [EmailService],
2960
+ })
2961
+ export class EmailModule {}
2962
+ `);
2963
+ } else {
2964
+ await writeFile(path20.join(apiDir, "src", "infra", "email", "email.service.ts"), `import nodemailer from 'nodemailer';
2965
+
2966
+ const transporter = nodemailer.createTransport({
2967
+ host: process.env.SMTP_HOST,
2968
+ port: Number(process.env.SMTP_PORT) || 587,
2969
+ secure: false,
2970
+ auth: {
2971
+ user: process.env.SMTP_USER,
2972
+ pass: process.env.SMTP_PASS,
2973
+ },
2974
+ });
2975
+
2976
+ export async function sendEmail(to: string, subject: string, html: string): Promise<void> {
2977
+ await transporter.sendMail({
2978
+ from: process.env.SMTP_FROM,
2979
+ to,
2980
+ subject,
2981
+ html,
2982
+ });
2983
+ }
2984
+
2985
+ export default { sendEmail };
2986
+ `);
2987
+ }
2988
+ await writeFile(path20.join(apiDir, "src", "infra", "email", "templates", "welcome.html"), `<!DOCTYPE html>
2989
+ <html>
2990
+ <head>
2991
+ <meta charset="utf-8">
2992
+ <style>
2993
+ body { font-family: system-ui, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
2994
+ .container { max-width: 600px; margin: 0 auto; background: white; border-radius: 8px; padding: 40px; }
2995
+ h1 { color: #333; }
2996
+ .btn { display: inline-block; padding: 12px 24px; background: #0070f3; color: white; text-decoration: none; border-radius: 6px; }
2997
+ </style>
2998
+ </head>
2999
+ <body>
3000
+ <div class="container">
3001
+ <h1>Bem-vindo ao ${config.projectName}!</h1>
3002
+ <p>Sua conta foi criada com sucesso.</p>
3003
+ <a href="{{APP_URL}}" class="btn">Acessar</a>
3004
+ </div>
3005
+ </body>
3006
+ </html>
3007
+ `);
3008
+ dependencies["nodemailer"] = dependencyVersionMap.nodemailer;
3009
+ devDependencies["@types/nodemailer"] = dependencyVersionMap["@types/nodemailer"];
3010
+ };
3011
+
3012
+ // src/installers/infra/minio.ts
3013
+ import path21 from "path";
3014
+ var minioInstaller = async (opts) => {
3015
+ const { config, apiDir, envEntries, dependencies } = opts;
3016
+ await ensureDir(path21.join(apiDir, "src", "infra", "storage"));
3017
+ envEntries.push(
3018
+ { key: "MINIO_ENDPOINT", value: "localhost", category: "MinIO Storage" },
3019
+ { key: "MINIO_PORT", value: "9000", category: "MinIO Storage" },
3020
+ { key: "MINIO_ACCESS_KEY", value: "minioadmin", category: "MinIO Storage" },
3021
+ { key: "MINIO_SECRET_KEY", value: "minioadmin", category: "MinIO Storage" },
3022
+ { key: "MINIO_BUCKET", value: config.projectName.replace(/-/g, "-"), category: "MinIO Storage" },
3023
+ { key: "MINIO_USE_SSL", value: "false", category: "MinIO Storage" }
3024
+ );
3025
+ if (config.backend === "nestjs") {
3026
+ await writeFile(path21.join(apiDir, "src", "infra", "storage", "storage.service.ts"), `import { Injectable, OnModuleInit } from '@nestjs/common';
3027
+ import { ConfigService } from '@nestjs/config';
3028
+ import * as Minio from 'minio';
3029
+
3030
+ @Injectable()
3031
+ export class StorageService implements OnModuleInit {
3032
+ private client: Minio.Client;
3033
+ private bucket: string;
3034
+
3035
+ constructor(private configService: ConfigService) {
3036
+ this.bucket = this.configService.get('MINIO_BUCKET', '${config.projectName}');
3037
+ this.client = new Minio.Client({
3038
+ endPoint: this.configService.get('MINIO_ENDPOINT', 'localhost'),
3039
+ port: this.configService.get('MINIO_PORT', 9000),
3040
+ useSSL: this.configService.get('MINIO_USE_SSL') === 'true',
3041
+ accessKey: this.configService.get('MINIO_ACCESS_KEY', 'minioadmin'),
3042
+ secretKey: this.configService.get('MINIO_SECRET_KEY', 'minioadmin'),
3043
+ });
3044
+ }
3045
+
3046
+ async onModuleInit() {
3047
+ const exists = await this.client.bucketExists(this.bucket);
3048
+ if (!exists) {
3049
+ await this.client.makeBucket(this.bucket);
3050
+ console.log(\`Bucket "\${this.bucket}" criado\`);
3051
+ }
3052
+ }
3053
+
3054
+ async upload(fileName: string, buffer: Buffer, contentType?: string): Promise<string> {
3055
+ await this.client.putObject(this.bucket, fileName, buffer, buffer.length, {
3056
+ 'Content-Type': contentType || 'application/octet-stream',
3057
+ });
3058
+ return fileName;
3059
+ }
3060
+
3061
+ async getFileUrl(fileName: string, expirySeconds = 3600): Promise<string> {
3062
+ return this.client.presignedGetObject(this.bucket, fileName, expirySeconds);
3063
+ }
3064
+
3065
+ async delete(fileName: string): Promise<void> {
3066
+ await this.client.removeObject(this.bucket, fileName);
3067
+ }
3068
+ }
3069
+ `);
3070
+ await writeFile(path21.join(apiDir, "src", "infra", "storage", "storage.module.ts"), `import { Global, Module } from '@nestjs/common';
3071
+ import { StorageService } from './storage.service';
3072
+
3073
+ @Global()
3074
+ @Module({
3075
+ providers: [StorageService],
3076
+ exports: [StorageService],
3077
+ })
3078
+ export class StorageModule {}
3079
+ `);
3080
+ } else {
3081
+ await writeFile(path21.join(apiDir, "src", "infra", "storage", "storage.ts"), `import * as Minio from 'minio';
3082
+
3083
+ const BUCKET = process.env.MINIO_BUCKET || '${config.projectName}';
3084
+
3085
+ const minioClient = new Minio.Client({
3086
+ endPoint: process.env.MINIO_ENDPOINT || 'localhost',
3087
+ port: Number(process.env.MINIO_PORT) || 9000,
3088
+ useSSL: process.env.MINIO_USE_SSL === 'true',
3089
+ accessKey: process.env.MINIO_ACCESS_KEY || 'minioadmin',
3090
+ secretKey: process.env.MINIO_SECRET_KEY || 'minioadmin',
3091
+ });
3092
+
3093
+ export async function initStorage(): Promise<void> {
3094
+ const exists = await minioClient.bucketExists(BUCKET);
3095
+ if (!exists) {
3096
+ await minioClient.makeBucket(BUCKET);
3097
+ console.log(\`Bucket "\${BUCKET}" criado\`);
3098
+ }
3099
+ }
3100
+
3101
+ export async function upload(fileName: string, buffer: Buffer, contentType?: string): Promise<string> {
3102
+ await minioClient.putObject(BUCKET, fileName, buffer, buffer.length, {
3103
+ 'Content-Type': contentType || 'application/octet-stream',
3104
+ });
3105
+ return fileName;
3106
+ }
3107
+
3108
+ export async function getFileUrl(fileName: string, expirySeconds = 3600): Promise<string> {
3109
+ return minioClient.presignedGetObject(BUCKET, fileName, expirySeconds);
3110
+ }
3111
+
3112
+ export async function deleteFile(fileName: string): Promise<void> {
3113
+ await minioClient.removeObject(BUCKET, fileName);
3114
+ }
3115
+
3116
+ export default { initStorage, upload, getFileUrl, deleteFile };
3117
+ `);
3118
+ }
3119
+ dependencies["minio"] = dependencyVersionMap.minio;
3120
+ };
3121
+
3122
+ // src/installers/infra/pm2.ts
3123
+ import path22 from "path";
3124
+ var pm2Installer = async (opts) => {
3125
+ const { config, projectDir, devDependencies, scripts } = opts;
3126
+ const ecosystem = `module.exports = {
3127
+ apps: [
3128
+ {
3129
+ name: '${config.projectName}-api',
3130
+ script: '${config.backend === "nestjs" ? "dist/main.js" : "dist/index.js"}',
3131
+ cwd: './apps/api',
3132
+ instances: 'max',
3133
+ exec_mode: 'cluster',
3134
+ env: {
3135
+ NODE_ENV: 'production',
3136
+ PORT: 3001,
3137
+ },
3138
+ watch: false,
3139
+ max_memory_restart: '1G',
3140
+ // Zero-downtime reload
3141
+ wait_ready: true,
3142
+ listen_timeout: 10000,
3143
+ kill_timeout: 5000,
3144
+ },
3145
+ ],
3146
+ };
3147
+ `;
3148
+ await writeFile(path22.join(projectDir, "ecosystem.config.cjs"), ecosystem);
3149
+ devDependencies["pm2"] = dependencyVersionMap.pm2;
3150
+ scripts["pm2:start"] = "pm2 start ecosystem.config.cjs";
3151
+ scripts["pm2:stop"] = "pm2 stop ecosystem.config.cjs";
3152
+ scripts["pm2:restart"] = "pm2 reload ecosystem.config.cjs";
3153
+ scripts["pm2:logs"] = "pm2 logs";
3154
+ };
3155
+
3156
+ // src/installers/multiTenant.ts
3157
+ import path23 from "path";
3158
+ var multiTenantInstaller = async (opts) => {
3159
+ const { config, apiDir } = opts;
3160
+ await ensureDir(path23.join(apiDir, "src", "tenant"));
3161
+ if (config.backend === "nestjs") {
3162
+ await writeFile(path23.join(apiDir, "src", "tenant", "tenant.middleware.ts"), `import { Injectable, NestMiddleware } from '@nestjs/common';
3163
+ import type { Request, Response, NextFunction } from 'express';
3164
+
3165
+ @Injectable()
3166
+ export class TenantMiddleware implements NestMiddleware {
3167
+ use(req: Request, _res: Response, next: NextFunction) {
3168
+ // Extrair tenant do header, subdomain ou path
3169
+ const tenantId = req.headers['x-tenant-id'] as string
3170
+ || this.extractFromSubdomain(req)
3171
+ || 'default';
3172
+
3173
+ (req as any).tenantId = tenantId;
3174
+ next();
3175
+ }
3176
+
3177
+ private extractFromSubdomain(req: Request): string | null {
3178
+ const host = req.hostname;
3179
+ const parts = host.split('.');
3180
+ if (parts.length >= 3) {
3181
+ return parts[0]; // subdomain como tenant
3182
+ }
3183
+ return null;
3184
+ }
3185
+ }
3186
+ `);
3187
+ await writeFile(path23.join(apiDir, "src", "tenant", "tenant.decorator.ts"), `import { createParamDecorator, ExecutionContext } from '@nestjs/common';
3188
+
3189
+ export const TenantId = createParamDecorator(
3190
+ (data: unknown, ctx: ExecutionContext): string => {
3191
+ const request = ctx.switchToHttp().getRequest();
3192
+ return request.tenantId || 'default';
3193
+ },
3194
+ );
3195
+ `);
3196
+ await writeFile(path23.join(apiDir, "src", "tenant", "tenant.module.ts"), `import { Module, MiddlewareConsumer, NestModule } from '@nestjs/common';
3197
+ import { TenantMiddleware } from './tenant.middleware';
3198
+
3199
+ @Module({})
3200
+ export class TenantModule implements NestModule {
3201
+ configure(consumer: MiddlewareConsumer) {
3202
+ consumer.apply(TenantMiddleware).forRoutes('*');
3203
+ }
3204
+ }
3205
+ `);
3206
+ } else if (config.backend === "express") {
3207
+ await writeFile(path23.join(apiDir, "src", "tenant", "tenant.middleware.ts"), `import type { Request, Response, NextFunction } from 'express';
3208
+
3209
+ export function tenantMiddleware(req: Request, _res: Response, next: NextFunction) {
3210
+ const tenantId = req.headers['x-tenant-id'] as string
3211
+ || extractFromSubdomain(req)
3212
+ || 'default';
3213
+
3214
+ (req as any).tenantId = tenantId;
3215
+ next();
3216
+ }
3217
+
3218
+ function extractFromSubdomain(req: Request): string | null {
3219
+ const host = req.hostname;
3220
+ const parts = host.split('.');
3221
+ if (parts.length >= 3) {
3222
+ return parts[0];
3223
+ }
3224
+ return null;
3225
+ }
3226
+ `);
3227
+ } else {
3228
+ await writeFile(path23.join(apiDir, "src", "tenant", "tenant.plugin.ts"), `import type { FastifyInstance, FastifyRequest } from 'fastify';
3229
+ import fp from 'fastify-plugin';
3230
+
3231
+ declare module 'fastify' {
3232
+ interface FastifyRequest {
3233
+ tenantId: string;
3234
+ }
3235
+ }
3236
+
3237
+ async function tenantPlugin(app: FastifyInstance) {
3238
+ app.decorateRequest('tenantId', 'default');
3239
+
3240
+ app.addHook('onRequest', async (request: FastifyRequest) => {
3241
+ const tenantId = (request.headers['x-tenant-id'] as string)
3242
+ || extractFromSubdomain(request)
3243
+ || 'default';
3244
+
3245
+ request.tenantId = tenantId;
3246
+ });
3247
+ }
3248
+
3249
+ function extractFromSubdomain(request: FastifyRequest): string | null {
3250
+ const host = request.hostname;
3251
+ const parts = host.split('.');
3252
+ if (parts.length >= 3) {
3253
+ return parts[0];
3254
+ }
3255
+ return null;
3256
+ }
3257
+
3258
+ export default fp(tenantPlugin, { name: 'tenant' });
3259
+ `);
3260
+ }
3261
+ };
3262
+
3263
+ // src/installers/integrations/viacep.ts
3264
+ import path24 from "path";
3265
+ var viacepInstaller = async (opts) => {
3266
+ const { config, apiDir, dependencies } = opts;
3267
+ await ensureDir(path24.join(apiDir, "src", "integrations", "viacep"));
3268
+ const serviceContent = `import axios from 'axios';
3269
+
3270
+ export interface ViaCepResponse {
3271
+ cep: string;
3272
+ logradouro: string;
3273
+ complemento: string;
3274
+ bairro: string;
3275
+ localidade: string;
3276
+ uf: string;
3277
+ ibge: string;
3278
+ erro?: boolean;
3279
+ }
3280
+
3281
+ const VIACEP_URL = 'https://viacep.com.br/ws';
3282
+
3283
+ export async function fetchAddress(cep: string): Promise<ViaCepResponse | null> {
3284
+ const cleanCep = cep.replace(/\\D/g, '');
3285
+ if (cleanCep.length !== 8) throw new Error('CEP deve ter 8 d\xEDgitos');
3286
+
3287
+ const { data } = await axios.get<ViaCepResponse>(\`\${VIACEP_URL}/\${cleanCep}/json\`);
3288
+ if (data.erro) return null;
3289
+ return data;
3290
+ }
3291
+ `;
3292
+ if (config.backend === "nestjs") {
3293
+ await writeFile(path24.join(apiDir, "src", "integrations", "viacep", "viacep.service.ts"), `import { Injectable } from '@nestjs/common';
3294
+ import axios from 'axios';
3295
+
3296
+ export interface ViaCepResponse {
3297
+ cep: string;
3298
+ logradouro: string;
3299
+ complemento: string;
3300
+ bairro: string;
3301
+ localidade: string;
3302
+ uf: string;
3303
+ ibge: string;
3304
+ erro?: boolean;
3305
+ }
3306
+
3307
+ @Injectable()
3308
+ export class ViaCepService {
3309
+ private readonly baseUrl = 'https://viacep.com.br/ws';
3310
+
3311
+ async fetchAddress(cep: string): Promise<ViaCepResponse | null> {
3312
+ const cleanCep = cep.replace(/\\D/g, '');
3313
+ if (cleanCep.length !== 8) throw new Error('CEP deve ter 8 d\xEDgitos');
3314
+
3315
+ const { data } = await axios.get<ViaCepResponse>(\`\${this.baseUrl}/\${cleanCep}/json\`);
3316
+ if (data.erro) return null;
3317
+ return data;
3318
+ }
3319
+ }
3320
+ `);
3321
+ } else {
3322
+ await writeFile(path24.join(apiDir, "src", "integrations", "viacep", "viacep.service.ts"), serviceContent);
3323
+ }
3324
+ dependencies["axios"] = dependencyVersionMap.axios;
3325
+ };
3326
+
3327
+ // src/installers/integrations/whatsapp.ts
3328
+ import path25 from "path";
3329
+ var whatsappInstaller = async (opts) => {
3330
+ const { config, apiDir, envEntries, dependencies } = opts;
3331
+ await ensureDir(path25.join(apiDir, "src", "integrations", "whatsapp"));
3332
+ envEntries.push(
3333
+ { key: "WHATSAPP_API_TOKEN", value: "", category: "WhatsApp", comment: "Token da API Meta WhatsApp Business" },
3334
+ { key: "WHATSAPP_PHONE_ID", value: "", category: "WhatsApp" },
3335
+ { key: "WHATSAPP_VERIFY_TOKEN", value: "my-verify-token", category: "WhatsApp", comment: "Token de verificacao do webhook" }
3336
+ );
3337
+ const serviceContent = config.backend === "nestjs" ? `import { Injectable } from '@nestjs/common';
3338
+ import { ConfigService } from '@nestjs/config';
3339
+ import axios from 'axios';
3340
+
3341
+ @Injectable()
3342
+ export class WhatsAppService {
3343
+ private readonly apiUrl: string;
3344
+ private readonly token: string;
3345
+
3346
+ constructor(private configService: ConfigService) {
3347
+ const phoneId = this.configService.get('WHATSAPP_PHONE_ID');
3348
+ this.apiUrl = \`https://graph.facebook.com/v18.0/\${phoneId}/messages\`;
3349
+ this.token = this.configService.get('WHATSAPP_API_TOKEN', '');
3350
+ }
3351
+
3352
+ async sendMessage(to: string, message: string): Promise<void> {
3353
+ await axios.post(
3354
+ this.apiUrl,
3355
+ {
3356
+ messaging_product: 'whatsapp',
3357
+ to,
3358
+ type: 'text',
3359
+ text: { body: message },
3360
+ },
3361
+ {
3362
+ headers: {
3363
+ Authorization: \`Bearer \${this.token}\`,
3364
+ 'Content-Type': 'application/json',
3365
+ },
3366
+ }
3367
+ );
3368
+ }
3369
+
3370
+ async sendTemplate(to: string, templateName: string, language = 'pt_BR'): Promise<void> {
3371
+ await axios.post(
3372
+ this.apiUrl,
3373
+ {
3374
+ messaging_product: 'whatsapp',
3375
+ to,
3376
+ type: 'template',
3377
+ template: { name: templateName, language: { code: language } },
3378
+ },
3379
+ {
3380
+ headers: {
3381
+ Authorization: \`Bearer \${this.token}\`,
3382
+ 'Content-Type': 'application/json',
3383
+ },
3384
+ }
3385
+ );
3386
+ }
3387
+ }
3388
+ ` : `import axios from 'axios';
3389
+
3390
+ const PHONE_ID = process.env.WHATSAPP_PHONE_ID;
3391
+ const API_URL = \`https://graph.facebook.com/v18.0/\${PHONE_ID}/messages\`;
3392
+ const TOKEN = process.env.WHATSAPP_API_TOKEN || '';
3393
+
3394
+ const headers = {
3395
+ Authorization: \`Bearer \${TOKEN}\`,
3396
+ 'Content-Type': 'application/json',
3397
+ };
3398
+
3399
+ export async function sendMessage(to: string, message: string): Promise<void> {
3400
+ await axios.post(API_URL, {
3401
+ messaging_product: 'whatsapp',
3402
+ to,
3403
+ type: 'text',
3404
+ text: { body: message },
3405
+ }, { headers });
3406
+ }
3407
+
3408
+ export async function sendTemplate(to: string, templateName: string, language = 'pt_BR'): Promise<void> {
3409
+ await axios.post(API_URL, {
3410
+ messaging_product: 'whatsapp',
3411
+ to,
3412
+ type: 'template',
3413
+ template: { name: templateName, language: { code: language } },
3414
+ }, { headers });
3415
+ }
3416
+ `;
3417
+ await writeFile(path25.join(apiDir, "src", "integrations", "whatsapp", "whatsapp.service.ts"), serviceContent);
3418
+ dependencies["axios"] = dependencyVersionMap.axios;
3419
+ };
3420
+
3421
+ // src/installers/integrations/stripe.ts
3422
+ import path26 from "path";
3423
+ var stripeInstaller = async (opts) => {
3424
+ const { config, apiDir, envEntries, dependencies } = opts;
3425
+ await ensureDir(path26.join(apiDir, "src", "integrations", "stripe"));
3426
+ envEntries.push(
3427
+ { key: "STRIPE_SECRET_KEY", value: "", category: "Stripe", comment: "Obtenha em https://dashboard.stripe.com" },
3428
+ { key: "STRIPE_WEBHOOK_SECRET", value: "", category: "Stripe" }
3429
+ );
3430
+ if (config.backend === "nestjs") {
3431
+ await writeFile(path26.join(apiDir, "src", "integrations", "stripe", "stripe.service.ts"), `import { Injectable } from '@nestjs/common';
3432
+ import { ConfigService } from '@nestjs/config';
3433
+ import Stripe from 'stripe';
3434
+
3435
+ @Injectable()
3436
+ export class StripeService {
3437
+ private stripe: Stripe;
3438
+
3439
+ constructor(private configService: ConfigService) {
3440
+ this.stripe = new Stripe(this.configService.get('STRIPE_SECRET_KEY', ''), {
3441
+ apiVersion: '2024-12-18.acacia',
3442
+ });
3443
+ }
3444
+
3445
+ async createCheckoutSession(params: {
3446
+ priceId: string;
3447
+ successUrl: string;
3448
+ cancelUrl: string;
3449
+ customerEmail?: string;
3450
+ }) {
3451
+ return this.stripe.checkout.sessions.create({
3452
+ mode: 'subscription',
3453
+ payment_method_types: ['card'],
3454
+ line_items: [{ price: params.priceId, quantity: 1 }],
3455
+ success_url: params.successUrl,
3456
+ cancel_url: params.cancelUrl,
3457
+ customer_email: params.customerEmail,
3458
+ });
3459
+ }
3460
+
3461
+ async createPaymentIntent(amount: number, currency = 'brl') {
3462
+ return this.stripe.paymentIntents.create({
3463
+ amount,
3464
+ currency,
3465
+ });
3466
+ }
3467
+
3468
+ constructWebhookEvent(body: Buffer, signature: string) {
3469
+ const webhookSecret = this.configService.get('STRIPE_WEBHOOK_SECRET', '');
3470
+ return this.stripe.webhooks.constructEvent(body, signature, webhookSecret);
3471
+ }
3472
+ }
3473
+ `);
3474
+ } else {
3475
+ await writeFile(path26.join(apiDir, "src", "integrations", "stripe", "stripe.service.ts"), `import Stripe from 'stripe';
3476
+
3477
+ const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', {
3478
+ apiVersion: '2024-12-18.acacia',
3479
+ });
3480
+
3481
+ export async function createCheckoutSession(params: {
3482
+ priceId: string;
3483
+ successUrl: string;
3484
+ cancelUrl: string;
3485
+ customerEmail?: string;
3486
+ }) {
3487
+ return stripe.checkout.sessions.create({
3488
+ mode: 'subscription',
3489
+ payment_method_types: ['card'],
3490
+ line_items: [{ price: params.priceId, quantity: 1 }],
3491
+ success_url: params.successUrl,
3492
+ cancel_url: params.cancelUrl,
3493
+ customer_email: params.customerEmail,
3494
+ });
3495
+ }
3496
+
3497
+ export async function createPaymentIntent(amount: number, currency = 'brl') {
3498
+ return stripe.paymentIntents.create({ amount, currency });
3499
+ }
3500
+
3501
+ export function constructWebhookEvent(body: Buffer, signature: string) {
3502
+ return stripe.webhooks.constructEvent(
3503
+ body,
3504
+ signature,
3505
+ process.env.STRIPE_WEBHOOK_SECRET || ''
3506
+ );
3507
+ }
3508
+ `);
3509
+ }
3510
+ dependencies["stripe"] = dependencyVersionMap.stripe;
3511
+ };
3512
+
3513
+ // src/installers/integrations/mercadoPago.ts
3514
+ import path27 from "path";
3515
+ var mercadoPagoInstaller = async (opts) => {
3516
+ const { config, apiDir, envEntries, dependencies } = opts;
3517
+ await ensureDir(path27.join(apiDir, "src", "integrations", "mercado-pago"));
3518
+ envEntries.push(
3519
+ { key: "MERCADOPAGO_ACCESS_TOKEN", value: "", category: "Mercado Pago", comment: "Obtenha em https://www.mercadopago.com.br/developers" }
3520
+ );
3521
+ if (config.backend === "nestjs") {
3522
+ await writeFile(path27.join(apiDir, "src", "integrations", "mercado-pago", "mercado-pago.service.ts"), `import { Injectable } from '@nestjs/common';
3523
+ import { ConfigService } from '@nestjs/config';
3524
+ import { MercadoPagoConfig, Preference, Payment } from 'mercadopago';
3525
+
3526
+ @Injectable()
3527
+ export class MercadoPagoService {
3528
+ private client: MercadoPagoConfig;
3529
+
3530
+ constructor(private configService: ConfigService) {
3531
+ this.client = new MercadoPagoConfig({
3532
+ accessToken: this.configService.get('MERCADOPAGO_ACCESS_TOKEN', ''),
3533
+ });
3534
+ }
3535
+
3536
+ async createPreference(params: {
3537
+ title: string;
3538
+ quantity: number;
3539
+ unitPrice: number;
3540
+ successUrl?: string;
3541
+ failureUrl?: string;
3542
+ }) {
3543
+ const preference = new Preference(this.client);
3544
+ return preference.create({
3545
+ body: {
3546
+ items: [
3547
+ {
3548
+ id: 'item-1',
3549
+ title: params.title,
3550
+ quantity: params.quantity,
3551
+ unit_price: params.unitPrice,
3552
+ currency_id: 'BRL',
3553
+ },
3554
+ ],
3555
+ back_urls: {
3556
+ success: params.successUrl || 'http://localhost:3000/success',
3557
+ failure: params.failureUrl || 'http://localhost:3000/failure',
3558
+ },
3559
+ auto_return: 'approved',
3560
+ },
3561
+ });
3562
+ }
3563
+
3564
+ async getPayment(paymentId: string) {
3565
+ const payment = new Payment(this.client);
3566
+ return payment.get({ id: paymentId });
3567
+ }
3568
+ }
3569
+ `);
3570
+ } else {
3571
+ await writeFile(path27.join(apiDir, "src", "integrations", "mercado-pago", "mercado-pago.service.ts"), `import { MercadoPagoConfig, Preference, Payment } from 'mercadopago';
3572
+
3573
+ const client = new MercadoPagoConfig({
3574
+ accessToken: process.env.MERCADOPAGO_ACCESS_TOKEN || '',
3575
+ });
3576
+
3577
+ export async function createPreference(params: {
3578
+ title: string;
3579
+ quantity: number;
3580
+ unitPrice: number;
3581
+ successUrl?: string;
3582
+ failureUrl?: string;
3583
+ }) {
3584
+ const preference = new Preference(client);
3585
+ return preference.create({
3586
+ body: {
3587
+ items: [
3588
+ {
3589
+ id: 'item-1',
3590
+ title: params.title,
3591
+ quantity: params.quantity,
3592
+ unit_price: params.unitPrice,
3593
+ currency_id: 'BRL',
3594
+ },
3595
+ ],
3596
+ back_urls: {
3597
+ success: params.successUrl || 'http://localhost:3000/success',
3598
+ failure: params.failureUrl || 'http://localhost:3000/failure',
3599
+ },
3600
+ auto_return: 'approved',
3601
+ },
3602
+ });
3603
+ }
3604
+
3605
+ export async function getPayment(paymentId: string) {
3606
+ const payment = new Payment(client);
3607
+ return payment.get({ id: paymentId });
3608
+ }
3609
+ `);
3610
+ }
3611
+ dependencies["mercadopago"] = dependencyVersionMap.mercadopago;
3612
+ };
3613
+
3614
+ // src/installers/integrations/abacatePay.ts
3615
+ import path28 from "path";
3616
+ var abacatePayInstaller = async (opts) => {
3617
+ const { config, apiDir, envEntries, dependencies } = opts;
3618
+ await ensureDir(path28.join(apiDir, "src", "integrations", "abacate-pay"));
3619
+ envEntries.push(
3620
+ { key: "ABACATEPAY_API_KEY", value: "", category: "AbacatePay", comment: "Obtenha em https://abacatepay.com" },
3621
+ { key: "ABACATEPAY_API_URL", value: "https://api.abacatepay.com/v1", category: "AbacatePay" }
3622
+ );
3623
+ if (config.backend === "nestjs") {
3624
+ await writeFile(path28.join(apiDir, "src", "integrations", "abacate-pay", "abacate-pay.service.ts"), `import { Injectable } from '@nestjs/common';
3625
+ import { ConfigService } from '@nestjs/config';
3626
+ import axios, { type AxiosInstance } from 'axios';
3627
+
3628
+ @Injectable()
3629
+ export class AbacatePayService {
3630
+ private client: AxiosInstance;
3631
+
3632
+ constructor(private configService: ConfigService) {
3633
+ this.client = axios.create({
3634
+ baseURL: this.configService.get('ABACATEPAY_API_URL', 'https://api.abacatepay.com/v1'),
3635
+ headers: {
3636
+ Authorization: \`Bearer \${this.configService.get('ABACATEPAY_API_KEY')}\`,
3637
+ 'Content-Type': 'application/json',
3638
+ },
3639
+ });
3640
+ }
3641
+
3642
+ async createPayment(params: {
3643
+ amount: number;
3644
+ description: string;
3645
+ customerEmail: string;
3646
+ }) {
3647
+ const { data } = await this.client.post('/payments', {
3648
+ amount: params.amount,
3649
+ description: params.description,
3650
+ customer: { email: params.customerEmail },
3651
+ });
3652
+ return data;
3653
+ }
3654
+
3655
+ async getPayment(paymentId: string) {
3656
+ const { data } = await this.client.get(\`/payments/\${paymentId}\`);
3657
+ return data;
3658
+ }
3659
+
3660
+ async createPixPayment(params: {
3661
+ amount: number;
3662
+ description: string;
3663
+ customerEmail: string;
3664
+ }) {
3665
+ const { data } = await this.client.post('/payments/pix', {
3666
+ amount: params.amount,
3667
+ description: params.description,
3668
+ customer: { email: params.customerEmail },
3669
+ });
3670
+ return data;
3671
+ }
3672
+ }
3673
+ `);
3674
+ } else {
3675
+ await writeFile(path28.join(apiDir, "src", "integrations", "abacate-pay", "abacate-pay.service.ts"), `import axios from 'axios';
3676
+
3677
+ const API_URL = process.env.ABACATEPAY_API_URL || 'https://api.abacatepay.com/v1';
3678
+ const API_KEY = process.env.ABACATEPAY_API_KEY || '';
3679
+
3680
+ const client = axios.create({
3681
+ baseURL: API_URL,
3682
+ headers: {
3683
+ Authorization: \`Bearer \${API_KEY}\`,
3684
+ 'Content-Type': 'application/json',
3685
+ },
3686
+ });
3687
+
3688
+ export async function createPayment(params: {
3689
+ amount: number;
3690
+ description: string;
3691
+ customerEmail: string;
3692
+ }) {
3693
+ const { data } = await client.post('/payments', {
3694
+ amount: params.amount,
3695
+ description: params.description,
3696
+ customer: { email: params.customerEmail },
3697
+ });
3698
+ return data;
3699
+ }
3700
+
3701
+ export async function getPayment(paymentId: string) {
3702
+ const { data } = await client.get(\`/payments/\${paymentId}\`);
3703
+ return data;
3704
+ }
3705
+
3706
+ export async function createPixPayment(params: {
3707
+ amount: number;
3708
+ description: string;
3709
+ customerEmail: string;
3710
+ }) {
3711
+ const { data } = await client.post('/payments/pix', {
3712
+ amount: params.amount,
3713
+ description: params.description,
3714
+ customer: { email: params.customerEmail },
3715
+ });
3716
+ return data;
3717
+ }
3718
+ `);
3719
+ }
3720
+ dependencies["axios"] = dependencyVersionMap.axios;
3721
+ };
3722
+
3723
+ // src/installers/index.ts
3724
+ function buildInstallerMap(config) {
3725
+ return {
3726
+ // Database (must run before auth since auth may depend on user model)
3727
+ "Prisma": {
3728
+ inUse: config.usePrisma,
3729
+ installer: prismaInstaller
3730
+ },
3731
+ "Mongoose": {
3732
+ inUse: config.useMongoose,
3733
+ installer: mongooseInstaller
3734
+ },
3735
+ // Multi-tenant (after database)
3736
+ "Multi-tenant": {
3737
+ inUse: config.multiTenant,
3738
+ installer: multiTenantInstaller
3739
+ },
3740
+ // Infrastructure
3741
+ "Redis": {
3742
+ inUse: config.redis,
3743
+ installer: redisInstaller
3744
+ },
3745
+ "SMTP": {
3746
+ inUse: config.smtp,
3747
+ installer: smtpInstaller
3748
+ },
3749
+ "MinIO": {
3750
+ inUse: config.minio,
3751
+ installer: minioInstaller
3752
+ },
3753
+ "PM2": {
3754
+ inUse: config.pm2,
3755
+ installer: pm2Installer
3756
+ },
3757
+ // Auth
3758
+ "JWT Auth": {
3759
+ inUse: config.auth.jwt,
3760
+ installer: jwtInstaller
3761
+ },
3762
+ "Magic Link": {
3763
+ inUse: config.auth.magicLink,
3764
+ installer: magicLinkInstaller
3765
+ },
3766
+ "Google OAuth": {
3767
+ inUse: config.auth.googleOAuth,
3768
+ installer: googleOauthInstaller
3769
+ },
3770
+ // Queues
3771
+ "BullMQ": {
3772
+ inUse: config.queue === "bullmq",
3773
+ installer: bullmqInstaller
3774
+ },
3775
+ "RabbitMQ": {
3776
+ inUse: config.queue === "rabbitmq",
3777
+ installer: rabbitmqInstaller
3778
+ },
3779
+ // Integrations
3780
+ "ViaCEP": {
3781
+ inUse: config.integrations.viacep,
3782
+ installer: viacepInstaller
3783
+ },
3784
+ "WhatsApp": {
3785
+ inUse: config.integrations.whatsapp,
3786
+ installer: whatsappInstaller
3787
+ },
3788
+ "Stripe": {
3789
+ inUse: config.integrations.stripe,
3790
+ installer: stripeInstaller
3791
+ },
3792
+ "Mercado Pago": {
3793
+ inUse: config.integrations.mercadoPago,
3794
+ installer: mercadoPagoInstaller
3795
+ },
3796
+ "AbacatePay": {
3797
+ inUse: config.integrations.abacatePay,
3798
+ installer: abacatePayInstaller
3799
+ }
3800
+ };
3801
+ }
3802
+
3803
+ // src/helpers/spinner.ts
3804
+ import ora from "ora";
3805
+ function createSpinner(text) {
3806
+ return ora({ text, color: "cyan" });
3807
+ }
3808
+ async function withSpinner(text, fn) {
3809
+ const spinner = createSpinner(text);
3810
+ spinner.start();
3811
+ try {
3812
+ const result = await fn();
3813
+ spinner.succeed();
3814
+ return result;
3815
+ } catch (error) {
3816
+ spinner.fail();
3817
+ throw error;
3818
+ }
3819
+ }
3820
+
3821
+ // src/helpers/git.ts
3822
+ import { execa as execa2 } from "execa";
3823
+ async function initGit(projectDir) {
3824
+ await execa2("git", ["init"], { cwd: projectDir });
3825
+ await execa2("git", ["add", "-A"], { cwd: projectDir });
3826
+ await execa2("git", ["commit", "-m", "chore: initial commit from plazercli"], {
3827
+ cwd: projectDir
3828
+ });
3829
+ }
3830
+
3831
+ // src/helpers/packages.ts
3832
+ import path29 from "path";
3833
+ async function readPackageJson(dir) {
3834
+ const content = await readFile(path29.join(dir, "package.json"));
3835
+ return JSON.parse(content);
3836
+ }
3837
+ async function writePackageJson(dir, pkg2) {
3838
+ await writeFile(path29.join(dir, "package.json"), JSON.stringify(pkg2, null, 2) + "\n");
3839
+ }
3840
+ function sortDeps(pkg2) {
3841
+ if (pkg2.dependencies) {
3842
+ pkg2.dependencies = Object.fromEntries(
3843
+ Object.entries(pkg2.dependencies).sort(([a], [b]) => a.localeCompare(b))
3844
+ );
3845
+ }
3846
+ if (pkg2.devDependencies) {
3847
+ pkg2.devDependencies = Object.fromEntries(
3848
+ Object.entries(pkg2.devDependencies).sort(([a], [b]) => a.localeCompare(b))
3849
+ );
3850
+ }
3851
+ return pkg2;
3852
+ }
3853
+
3854
+ // src/generators/project.ts
3855
+ async function generateProject(config) {
3856
+ const projectDir = config.projectDir;
3857
+ const apiDir = path30.join(projectDir, "apps", "api");
3858
+ const webDir = path30.join(projectDir, "apps", "web");
3859
+ const envEntries = [];
3860
+ const dependencies = {};
3861
+ const devDependencies = {};
3862
+ const scripts = {};
3863
+ const dockerServices = [];
3864
+ const opts = {
3865
+ config,
3866
+ projectDir,
3867
+ apiDir,
3868
+ webDir,
3869
+ envEntries,
3870
+ dependencies,
3871
+ devDependencies,
3872
+ scripts,
3873
+ dockerServices
3874
+ };
3875
+ console.log(chalk2.cyan("\n Liz est\xE1 preparando seu projeto...\n"));
3876
+ await withSpinner("Criando estrutura do monorepo...", async () => {
3877
+ await generateBase(config);
3878
+ });
3879
+ await withSpinner(`Configurando frontend (${config.frontend})...`, async () => {
3880
+ await generateFrontend(config, webDir);
3881
+ });
3882
+ await withSpinner(`Configurando backend (${config.backend})...`, async () => {
3883
+ await generateBackend(config, apiDir);
3884
+ });
3885
+ const installerMap = buildInstallerMap(config);
3886
+ for (const [name, entry] of Object.entries(installerMap)) {
3887
+ if (entry.inUse) {
3888
+ await withSpinner(`Configurando ${name}...`, async () => {
3889
+ await entry.installer(opts);
3890
+ });
3891
+ }
3892
+ }
3893
+ if (Object.keys(dependencies).length > 0 || Object.keys(devDependencies).length > 0 || Object.keys(scripts).length > 0) {
3894
+ const apiPkg = await readPackageJson(apiDir);
3895
+ if (!apiPkg.dependencies) apiPkg.dependencies = {};
3896
+ if (!apiPkg.devDependencies) apiPkg.devDependencies = {};
3897
+ if (!apiPkg.scripts) apiPkg.scripts = {};
3898
+ Object.assign(apiPkg.dependencies, dependencies);
3899
+ Object.assign(apiPkg.devDependencies, devDependencies);
3900
+ Object.assign(apiPkg.scripts, scripts);
3901
+ await writePackageJson(apiDir, sortDeps(apiPkg));
3902
+ }
3903
+ await withSpinner("Conectando m\xF3dulos automaticamente...", async () => {
3904
+ await generateWiring(config, apiDir);
3905
+ });
3906
+ if (config.database !== "none" || config.redis || config.queue === "rabbitmq" || config.minio) {
3907
+ await withSpinner("Gerando docker-compose.yml...", async () => {
3908
+ await generateDocker(config, projectDir, dockerServices);
3909
+ });
3910
+ }
3911
+ await withSpinner("Gerando arquivos .env...", async () => {
3912
+ await generateEnv(config, apiDir, envEntries);
3913
+ });
3914
+ await withSpinner("Gerando README.md...", async () => {
3915
+ await generateReadme(config, projectDir);
3916
+ });
3917
+ await withSpinner("Configurando Liz (AI Agent)...", async () => {
3918
+ await generateLiz(config, projectDir);
3919
+ });
3920
+ let installResult = { success: false };
3921
+ try {
3922
+ await withSpinner(`Instalando depend\xEAncias (${config.runtime})...`, async () => {
3923
+ installResult = await installDependencies(projectDir, config.runtime);
3924
+ });
3925
+ if (installResult.fallback) {
3926
+ logger.warn(`${config.runtime} n\xE3o encontrado. Depend\xEAncias instaladas com ${installResult.fallback}.`);
3927
+ }
3928
+ } catch {
3929
+ logger.warn(`N\xE3o foi poss\xEDvel instalar depend\xEAncias automaticamente.`);
3930
+ logger.info(`Rode manualmente: cd ${config.projectName} && ${config.runtime} install`);
3931
+ }
3932
+ try {
3933
+ await withSpinner("Inicializando reposit\xF3rio git...", async () => {
3934
+ await initGit(projectDir);
3935
+ });
3936
+ } catch {
3937
+ logger.warn("Git n\xE3o encontrado. Inicialize manualmente com: git init");
3938
+ }
3939
+ printNextSteps(config, installResult.success);
3940
+ }
3941
+ function printNextSteps(config, depsInstalled = true) {
3942
+ const runCmd = getRunCommand(config.runtime);
3943
+ const hasDocker = config.database !== "none" || config.redis || config.queue === "rabbitmq" || config.minio;
3944
+ const authMethods = [
3945
+ config.auth.jwt && "JWT",
3946
+ config.auth.magicLink && "Magic Link",
3947
+ config.auth.googleOAuth && "Google OAuth"
3948
+ ].filter(Boolean);
3949
+ const integrations = Object.entries(config.integrations).filter(([, v]) => v).map(([k]) => k);
3950
+ console.log(
3951
+ chalk2.bold.green(`
3952
+ \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
3953
+ \u2551 \u2551
3954
+ \u2551 \u2705 Projeto "${config.projectName}" criado! \u2551
3955
+ \u2551 Tudo configurado pela Liz \u2551
3956
+ \u2551 \u2551
3957
+ \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
3958
+ `)
3959
+ );
3960
+ console.log(chalk2.bold.white(" O que a Liz configurou pra voc\xEA:\n"));
3961
+ console.log(chalk2.green(" \u2714 ") + chalk2.white(`Frontend ${chalk2.cyan(config.frontend)} pronto em apps/web/`));
3962
+ console.log(chalk2.green(" \u2714 ") + chalk2.white(`Backend ${chalk2.cyan(config.backend)} pronto em apps/api/`));
3963
+ if (config.database !== "none") {
3964
+ console.log(chalk2.green(" \u2714 ") + chalk2.white(`Banco ${chalk2.cyan(config.database)} configurado com nome "${chalk2.cyan(config.projectName.replace(/-/g, "_"))}"`));
3965
+ }
3966
+ if (authMethods.length > 0) {
3967
+ console.log(chalk2.green(" \u2714 ") + chalk2.white(`Autentica\xE7\xE3o: ${chalk2.cyan(authMethods.join(", "))} com rotas prontas`));
3968
+ }
3969
+ if (config.redis) {
3970
+ console.log(chalk2.green(" \u2714 ") + chalk2.white(`Redis configurado e conectado`));
3971
+ }
3972
+ if (config.queue !== "none") {
3973
+ console.log(chalk2.green(" \u2714 ") + chalk2.white(`Filas ${chalk2.cyan(config.queue)} com worker de exemplo`));
3974
+ }
3975
+ if (config.smtp) {
3976
+ console.log(chalk2.green(" \u2714 ") + chalk2.white(`SMTP com servi\xE7o de email e template HTML`));
3977
+ }
3978
+ if (config.minio) {
3979
+ console.log(chalk2.green(" \u2714 ") + chalk2.white(`MinIO storage com upload/download prontos`));
3980
+ }
3981
+ if (config.multiTenant) {
3982
+ console.log(chalk2.green(" \u2714 ") + chalk2.white(`Multi-tenant com middleware de isolamento`));
3983
+ }
3984
+ if (config.pm2) {
3985
+ console.log(chalk2.green(" \u2714 ") + chalk2.white(`PM2 com cluster mode para zero-downtime`));
3986
+ }
3987
+ if (integrations.length > 0) {
3988
+ console.log(chalk2.green(" \u2714 ") + chalk2.white(`Integra\xE7\xF5es: ${chalk2.cyan(integrations.join(", "))}`));
3989
+ }
3990
+ console.log(chalk2.green(" \u2714 ") + chalk2.white(`Liz (AI Agent) configurada para te ajudar`));
3991
+ if (hasDocker) {
3992
+ console.log(chalk2.green(" \u2714 ") + chalk2.white(`Docker Compose com todos os servi\xE7os`));
3993
+ }
3994
+ console.log(chalk2.bold.white("\n Pr\xF3ximos passos:\n"));
3995
+ let step = 1;
3996
+ console.log(chalk2.white(` ${step}. ${chalk2.cyan(`cd ${config.projectName}`)}`));
3997
+ step++;
3998
+ if (!depsInstalled) {
3999
+ console.log(chalk2.white(` ${step}. ${chalk2.cyan(`${config.runtime} install`)}`));
4000
+ console.log(chalk2.gray(" Instala as depend\xEAncias do projeto"));
4001
+ step++;
4002
+ }
4003
+ if (hasDocker) {
4004
+ console.log(chalk2.white(` ${step}. ${chalk2.cyan("docker compose up -d")}`));
4005
+ console.log(chalk2.gray(" Sobe banco, redis, minio, etc em containers"));
4006
+ step++;
4007
+ }
4008
+ if (config.usePrisma) {
4009
+ console.log(chalk2.white(` ${step}. ${chalk2.cyan("cd apps/api && npx prisma db push && cd ../..")}`));
4010
+ console.log(chalk2.gray(" Cria as tabelas no banco de dados"));
4011
+ step++;
4012
+ }
4013
+ console.log(chalk2.white(` ${step}. ${chalk2.cyan(`${runCmd} dev`)}`));
4014
+ console.log(chalk2.gray(" Inicia frontend e API em modo desenvolvimento"));
4015
+ console.log(chalk2.bold.white("\n URLs:\n"));
4016
+ console.log(chalk2.white(` Frontend: ${chalk2.cyan("http://localhost:3000")}`));
4017
+ console.log(chalk2.white(` API: ${chalk2.cyan("http://localhost:3001")}`));
4018
+ console.log(chalk2.white(` Health: ${chalk2.cyan("http://localhost:3001/api/health")}`));
4019
+ if (config.usePrisma) {
4020
+ console.log(chalk2.white(` DB Studio: ${chalk2.cyan("npx prisma studio")} (na pasta apps/api)`));
4021
+ }
4022
+ if (config.queue === "rabbitmq") {
4023
+ console.log(chalk2.white(` RabbitMQ: ${chalk2.cyan("http://localhost:15672")} (guest/guest)`));
4024
+ }
4025
+ if (config.minio) {
4026
+ console.log(chalk2.white(` MinIO: ${chalk2.cyan("http://localhost:9001")} (minioadmin/minioadmin)`));
4027
+ }
4028
+ if (config.pm2) {
4029
+ console.log(chalk2.bold.white("\n Produ\xE7\xE3o com PM2:\n"));
4030
+ console.log(chalk2.white(` ${chalk2.cyan(`${runCmd} build`)} && ${chalk2.cyan("pm2 start ecosystem.config.cjs")}`));
4031
+ }
4032
+ console.log(chalk2.gray(`
4033
+ Precisa de ajuda? A Liz est\xE1 dispon\xEDvel no seu editor.`));
4034
+ console.log(chalk2.gray(` Documenta\xE7\xE3o: https://github.com/pablocarss/plazercli
4035
+ `));
4036
+ }
4037
+
4038
+ // src/index.ts
4039
+ import { confirm as confirm2 } from "@inquirer/prompts";
4040
+ async function main() {
4041
+ const flags = parseArgs();
4042
+ printBanner();
4043
+ const config = await runPrompts(flags);
4044
+ printSummary(config);
4045
+ const confirmed = await confirm2({
4046
+ message: "\u2705 Confirma a cria\xE7\xE3o do projeto com essas configura\xE7\xF5es?",
4047
+ default: true
4048
+ });
4049
+ if (!confirmed) {
4050
+ logger.warn("Cria\xE7\xE3o cancelada.");
4051
+ process.exit(0);
4052
+ }
4053
+ await generateProject(config);
4054
+ }
4055
+ main().catch((error) => {
4056
+ logger.error(error instanceof Error ? error.message : String(error));
4057
+ process.exit(1);
4058
+ });
4059
+ //# sourceMappingURL=index.js.map