@thinhnguyencth1204/nextcli 0.4.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -137,7 +137,7 @@ async function replaceTokensInDirectory(directoryPath, replacements) {
137
137
  }
138
138
  }
139
139
  }
140
- async function mergeEnvFile(envFilePath, entries) {
140
+ async function mergeEnvFile(envFilePath, entries, options = {}) {
141
141
  const envExists = await pathExists(envFilePath);
142
142
  const currentContent = envExists ? await readFile(envFilePath, "utf8") : "";
143
143
  const lines = currentContent ? currentContent.split("\n") : [];
@@ -161,6 +161,18 @@ async function mergeEnvFile(envFilePath, entries) {
161
161
  if (additions.length === 0) {
162
162
  return;
163
163
  }
164
+ if (options.header && currentContent.includes(options.header)) {
165
+ const nextContent2 = currentContent.replace(
166
+ options.header,
167
+ `${options.header}
168
+ ${additions.join("\n")}`
169
+ );
170
+ await writeFile(envFilePath, nextContent2, "utf8");
171
+ return;
172
+ }
173
+ if (options.header) {
174
+ additions.unshift(options.header);
175
+ }
164
176
  const separator = currentContent.endsWith("\n") || currentContent.length === 0 ? "" : "\n";
165
177
  const nextContent = `${currentContent}${separator}${additions.join("\n")}
166
178
  `;
@@ -202,10 +214,9 @@ var rootDir = path2.resolve(currentDir, "../");
202
214
  var templatePaths = {
203
215
  base: path2.join(rootDir, "templates/next-base"),
204
216
  chat: path2.join(rootDir, "templates/features/chat"),
205
- supabase: path2.join(rootDir, "templates/features/supabase"),
206
217
  supabaseRealtime: path2.join(rootDir, "templates/features/supabase-realtime"),
207
218
  seo: path2.join(rootDir, "templates/features/seo"),
208
- resend: path2.join(rootDir, "templates/features/resend")
219
+ email: path2.join(rootDir, "templates/features/email")
209
220
  };
210
221
 
211
222
  // src/core/modules.ts
@@ -223,25 +234,6 @@ var optionalModules = [
223
234
  | \`NEXT_PUBLIC_ENABLE_CHAT\` | Set \`true\` when chat module is enabled (auto on add) |
224
235
 
225
236
  Requires \`supabase-realtime\` (auto-added). Run \`db:migrate\` after add \u2014 chat Prisma models are appended.`
226
- },
227
- {
228
- id: "supabase",
229
- label: "Supabase",
230
- description: "Adds Supabase client and storage upload helpers",
231
- templatePath: templatePaths.supabase,
232
- env: {
233
- NEXT_PUBLIC_SUPABASE_URL: "",
234
- NEXT_PUBLIC_SUPABASE_ANON_KEY: "",
235
- NEXT_PUBLIC_SUPABASE_STORAGE_BUCKET: "public"
236
- },
237
- dependencies: {
238
- "@supabase/supabase-js": "^2.44.2"
239
- },
240
- setupSection: `| Variable | Where to get |
241
- | -------- | ------------ |
242
- | \`NEXT_PUBLIC_SUPABASE_URL\` | Supabase Dashboard \u2192 Project Settings \u2192 API \u2192 Project URL |
243
- | \`NEXT_PUBLIC_SUPABASE_ANON_KEY\` | Same page \u2192 Project API keys \u2192 \`anon\` \`public\` |
244
- | \`NEXT_PUBLIC_SUPABASE_STORAGE_BUCKET\` | Storage \u2192 create bucket \u2192 use bucket name (default scaffold: \`public\`) |`
245
237
  },
246
238
  {
247
239
  id: "supabase-realtime",
@@ -255,7 +247,7 @@ Requires \`supabase-realtime\` (auto-added). Run \`db:migrate\` after add \u2014
255
247
  dependencies: {
256
248
  "@supabase/supabase-js": "^2.44.2"
257
249
  },
258
- setupSection: `Uses same Supabase URL/anon key as \`supabase\` module. Enable Realtime on tables in Supabase Dashboard \u2192 Database \u2192 Replication.`
250
+ setupSection: `Uses the base Supabase URL/anon key. Enable Realtime on tables in Supabase Dashboard \u2192 Database \u2192 Replication.`
259
251
  },
260
252
  {
261
253
  id: "seo",
@@ -266,23 +258,17 @@ Requires \`supabase-realtime\` (auto-added). Run \`db:migrate\` after add \u2014
266
258
  setupSection: `No extra env keys. Edit \`src/app/robots.ts\`, \`sitemap.ts\`, and JSON-LD helpers after add.`
267
259
  },
268
260
  {
269
- id: "resend",
270
- label: "Resend email",
271
- description: "Adds Resend client, send helper, and React Email welcome template",
272
- templatePath: templatePaths.resend,
273
- env: {
274
- RESEND_API_KEY: "",
275
- RESEND_FROM_EMAIL: ""
276
- },
277
- dependencies: {
278
- resend: "^6.9.2",
279
- "@react-email/components": "^1.0.12",
280
- "react-email": "^4.0.0"
281
- },
282
- setupSection: `| Variable | Where to get |
283
- | -------- | ------------ |
284
- | \`RESEND_API_KEY\` | resend.com \u2192 API Keys \u2192 Create |
285
- | \`RESEND_FROM_EMAIL\` | resend.com \u2192 Domains \u2192 verify domain \u2192 use \`Name <you@domain.com>\` |`
261
+ id: "email",
262
+ label: "Email module (SMTP or Resend)",
263
+ description: "Adds provider-agnostic sendEmail helper with SMTP/Resend adapters",
264
+ templatePath: templatePaths.email,
265
+ env: {},
266
+ setupSection: `When you add this module, NexTCLI asks which provider to configure:
267
+
268
+ - **Resend**: \`EMAIL_PROVIDER=resend\`, \`RESEND_API_KEY\`, \`RESEND_FROM_EMAIL\`
269
+ - **SMTP**: \`EMAIL_PROVIDER=smtp\`, \`SMTP_HOST\`, \`SMTP_PORT\`, \`SMTP_USER\`, \`SMTP_PASSWORD\`, \`SMTP_FROM\`
270
+
271
+ Only the selected provider keys are merged into your env files.`
286
272
  }
287
273
  ];
288
274
  function getModuleById(moduleId) {
@@ -456,6 +442,42 @@ async function ensureBetterAuthGenerate(cwd, options = {}) {
456
442
  }
457
443
  }
458
444
 
445
+ // src/core/email-module.ts
446
+ var EMAIL_PROVIDER_OPTIONS = ["resend", "smtp"];
447
+ function isEmailProvider(value) {
448
+ return EMAIL_PROVIDER_OPTIONS.includes(value);
449
+ }
450
+ function getEmailProviderEnv(provider) {
451
+ if (provider === "smtp") {
452
+ return {
453
+ EMAIL_PROVIDER: "smtp",
454
+ SMTP_HOST: "",
455
+ SMTP_PORT: "587",
456
+ SMTP_USER: "",
457
+ SMTP_PASSWORD: "",
458
+ SMTP_FROM: ""
459
+ };
460
+ }
461
+ return {
462
+ EMAIL_PROVIDER: "resend",
463
+ RESEND_API_KEY: "",
464
+ RESEND_FROM_EMAIL: ""
465
+ };
466
+ }
467
+ function getEmailProviderDependencies(provider) {
468
+ if (provider === "smtp") {
469
+ return {
470
+ nodemailer: "^6.10.1",
471
+ "@types/nodemailer": "^6.4.17"
472
+ };
473
+ }
474
+ return {
475
+ resend: "^6.9.2",
476
+ "@react-email/components": "^1.0.12",
477
+ "react-email": "^4.0.0"
478
+ };
479
+ }
480
+
459
481
  // src/core/chat-schema.ts
460
482
  import path3 from "path";
461
483
  import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
@@ -560,7 +582,7 @@ import { readdir as readdir3, readFile as readFile4, writeFile as writeFile4 } f
560
582
  import path4 from "path";
561
583
  import { readdir as readdir2, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
562
584
  var defaultManifest = {
563
- cli: "0.4.2",
585
+ cli: "0.6.0",
564
586
  defaultLocale: "vi",
565
587
  locales: ["vi"],
566
588
  namespaces: ["common", "auth", "example"],
@@ -625,7 +647,7 @@ async function reconcileManifest(projectDir) {
625
647
  ]
626
648
  };
627
649
  merged.defaultLocale = merged.locales.includes(merged.defaultLocale) ? merged.defaultLocale : merged.locales[0] ?? "vi";
628
- merged.modules = [...new Set(merged.modules)];
650
+ merged.modules = [...new Set(merged.modules)].map((moduleId) => moduleId === "resend" ? "email" : moduleId).filter((moduleId) => moduleId !== "supabase");
629
651
  merged.features = [...new Set(merged.features)];
630
652
  await writeManifest(projectDir, merged);
631
653
  return merged;
@@ -1307,24 +1329,6 @@ function normalizeModuleSelection(moduleIds) {
1307
1329
  autoAddedModules: autoAdded
1308
1330
  };
1309
1331
  }
1310
- async function upsertEnvValue(envFilePath, key, value) {
1311
- if (!await pathExists(envFilePath)) {
1312
- return;
1313
- }
1314
- const content = await readFile6(envFilePath, "utf8");
1315
- const entry = `${key}=${value}`;
1316
- const pattern = new RegExp(`^${key}=.*$`, "m");
1317
- if (pattern.test(content)) {
1318
- const next = content.replace(pattern, entry);
1319
- if (next !== content) {
1320
- await writeFile6(envFilePath, next, "utf8");
1321
- }
1322
- return;
1323
- }
1324
- const separator = content.endsWith("\n") || content.length === 0 ? "" : "\n";
1325
- await writeFile6(envFilePath, `${content}${separator}${entry}
1326
- `, "utf8");
1327
- }
1328
1332
  function registerAddCommand(program2) {
1329
1333
  const add = program2.command("add").description("Add modules to an existing app");
1330
1334
  add.command("feature").description("Scaffold a feature folder under src/features").argument("<feature-name>", "Feature name").action(async (featureName) => {
@@ -1443,167 +1447,239 @@ function registerAddCommand(program2) {
1443
1447
  "No migration was executed. Run your migration command manually when ready."
1444
1448
  );
1445
1449
  });
1446
- add.command("module").description("Add optional modules using interactive multi-select").option("--module <module...>", "Preselect module ids").option("--yes", "Skip prompts").action(async (options) => {
1447
- const cwd = process.cwd();
1448
- const hasSrc = await pathExists(path7.join(cwd, "src"));
1449
- const hasPackageJson = await pathExists(path7.join(cwd, "package.json"));
1450
- if (!hasSrc || !hasPackageJson) {
1451
- log.error("Run this command from your generated Next.js project root.");
1452
- process.exitCode = 1;
1453
- return;
1454
- }
1455
- const validIds = new Set(optionalModules.map((module) => module.id));
1456
- const requestedIds = options.module ? options.module.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean) : [];
1457
- for (const moduleId of requestedIds) {
1458
- if (!validIds.has(moduleId)) {
1459
- log.error(`Unknown module: ${moduleId}`);
1450
+ add.command("module").description("Add optional modules using interactive multi-select").option("--module <module...>", "Preselect module ids").option(
1451
+ "--email-provider <provider>",
1452
+ "Email provider for email module: resend or smtp"
1453
+ ).option("--yes", "Skip prompts").action(
1454
+ async (options) => {
1455
+ const cwd = process.cwd();
1456
+ const hasSrc = await pathExists(path7.join(cwd, "src"));
1457
+ const hasPackageJson = await pathExists(path7.join(cwd, "package.json"));
1458
+ if (!hasSrc || !hasPackageJson) {
1459
+ log.error(
1460
+ "Run this command from your generated Next.js project root."
1461
+ );
1460
1462
  process.exitCode = 1;
1461
1463
  return;
1462
1464
  }
1463
- }
1464
- startPrompt("NexTCLI optional modules");
1465
- const rawModules = requestedIds.length > 0 ? [...new Set(requestedIds)] : options.yes ? [] : await askMultiSelect(
1466
- "Select modules to add:",
1467
- optionalModules.map((module) => ({
1468
- value: module.id,
1469
- label: module.label,
1470
- hint: module.description
1471
- })),
1472
- []
1473
- );
1474
- const { selectedModules, autoAddedModules } = normalizeModuleSelection(rawModules);
1475
- if (selectedModules.length === 0) {
1476
- finishPrompt("No modules selected.");
1477
- return;
1478
- }
1479
- const skippedConflictsByModule = [];
1480
- let copiedFileCount = 0;
1481
- for (const moduleId of selectedModules) {
1482
- const module = getModuleById(moduleId);
1483
- const copyReport = await copyDirectorySafely(module.templatePath, cwd);
1484
- copiedFileCount += copyReport.copiedCount;
1485
- if (copyReport.skippedConflicts.length > 0) {
1486
- skippedConflictsByModule.push({
1487
- moduleId,
1488
- paths: copyReport.skippedConflicts
1489
- });
1465
+ const validIds = new Set(
1466
+ optionalModules.map((module) => module.id)
1467
+ );
1468
+ const requestedValues = options.module ? options.module.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean) : [];
1469
+ const requestedIds = [];
1470
+ let emailProviderFromModuleAlias;
1471
+ const optionEmailProvider = options.emailProvider ? options.emailProvider.trim().toLowerCase() : void 0;
1472
+ if (optionEmailProvider && !isEmailProvider(optionEmailProvider)) {
1473
+ log.error(`Unknown email provider: ${options.emailProvider}`);
1474
+ process.exitCode = 1;
1475
+ return;
1490
1476
  }
1491
- }
1492
- let chatSchemaStatus;
1493
- if (selectedModules.includes("chat")) {
1494
- chatSchemaStatus = await ensureChatSchemaInProject(cwd);
1495
- }
1496
- const envEntries = selectedModules.reduce(
1497
- (acc, moduleId) => {
1477
+ for (const moduleId of requestedValues) {
1478
+ if (validIds.has(moduleId)) {
1479
+ requestedIds.push(moduleId);
1480
+ continue;
1481
+ }
1482
+ if (moduleId === "supabase") {
1483
+ log.info(
1484
+ "Supabase is now part of the base template; only supabase-realtime is optional."
1485
+ );
1486
+ continue;
1487
+ }
1488
+ if (moduleId === "resend") {
1489
+ log.info(
1490
+ "Module 'resend' is now 'email'; selecting email with provider resend."
1491
+ );
1492
+ requestedIds.push("email");
1493
+ emailProviderFromModuleAlias = "resend";
1494
+ continue;
1495
+ }
1496
+ if (!validIds.has(moduleId)) {
1497
+ log.error(`Unknown module: ${moduleId}`);
1498
+ process.exitCode = 1;
1499
+ return;
1500
+ }
1501
+ }
1502
+ startPrompt("NexTCLI optional modules");
1503
+ const rawModules = requestedIds.length > 0 ? [...new Set(requestedIds)] : options.yes ? [] : await askMultiSelect(
1504
+ "Select modules to add:",
1505
+ optionalModules.map((module) => ({
1506
+ value: module.id,
1507
+ label: module.label,
1508
+ hint: module.description
1509
+ })),
1510
+ []
1511
+ );
1512
+ const { selectedModules, autoAddedModules } = normalizeModuleSelection(rawModules);
1513
+ if (optionEmailProvider && !selectedModules.includes("email")) {
1514
+ log.warn(
1515
+ "Ignoring --email-provider because email module is not selected."
1516
+ );
1517
+ }
1518
+ let emailProvider;
1519
+ if (selectedModules.includes("email")) {
1520
+ if (emailProviderFromModuleAlias) {
1521
+ emailProvider = emailProviderFromModuleAlias;
1522
+ } else if (optionEmailProvider && isEmailProvider(optionEmailProvider)) {
1523
+ emailProvider = optionEmailProvider;
1524
+ } else if (options.yes) {
1525
+ emailProvider = "resend";
1526
+ log.info(
1527
+ "No --email-provider supplied in --yes mode; defaulting email provider to resend."
1528
+ );
1529
+ } else {
1530
+ emailProvider = await askSelect(
1531
+ "Select email provider:",
1532
+ [
1533
+ {
1534
+ value: "resend",
1535
+ label: "Resend",
1536
+ hint: "Transactional email API with React Email support"
1537
+ },
1538
+ {
1539
+ value: "smtp",
1540
+ label: "SMTP",
1541
+ hint: "Use your SMTP relay credentials"
1542
+ }
1543
+ ],
1544
+ "resend"
1545
+ );
1546
+ }
1547
+ }
1548
+ if (selectedModules.length === 0) {
1549
+ finishPrompt("No modules selected.");
1550
+ return;
1551
+ }
1552
+ const skippedConflictsByModule = [];
1553
+ let copiedFileCount = 0;
1554
+ for (const moduleId of selectedModules) {
1498
1555
  const module = getModuleById(moduleId);
1499
- return {
1500
- ...acc,
1501
- ...module.env
1502
- };
1503
- },
1504
- {}
1505
- );
1506
- if (Object.keys(envEntries).length > 0) {
1507
- const envTargets = [".env", ".env.example", ".env.development"];
1508
- for (const envFile of envTargets) {
1509
- const envPath = path7.join(cwd, envFile);
1510
- if (await pathExists(envPath)) {
1511
- await mergeEnvFile(envPath, envEntries);
1556
+ const copyReport = await copyDirectorySafely(
1557
+ module.templatePath,
1558
+ cwd
1559
+ );
1560
+ copiedFileCount += copyReport.copiedCount;
1561
+ if (copyReport.skippedConflicts.length > 0) {
1562
+ skippedConflictsByModule.push({
1563
+ moduleId,
1564
+ paths: copyReport.skippedConflicts
1565
+ });
1512
1566
  }
1513
1567
  }
1514
- }
1515
- if (selectedModules.includes("chat")) {
1568
+ let chatSchemaStatus;
1569
+ if (selectedModules.includes("chat")) {
1570
+ chatSchemaStatus = await ensureChatSchemaInProject(cwd);
1571
+ }
1516
1572
  const envTargets = [".env", ".env.example", ".env.development"];
1517
- for (const envFile of envTargets) {
1518
- await upsertEnvValue(
1519
- path7.join(cwd, envFile),
1520
- "NEXT_PUBLIC_ENABLE_CHAT",
1521
- "true"
1522
- );
1573
+ for (const moduleId of selectedModules) {
1574
+ const module = getModuleById(moduleId);
1575
+ const moduleEnv = moduleId === "email" && emailProvider ? getEmailProviderEnv(emailProvider) : module.env;
1576
+ if (Object.keys(moduleEnv).length === 0) {
1577
+ continue;
1578
+ }
1579
+ for (const envFile of envTargets) {
1580
+ const envPath = path7.join(cwd, envFile);
1581
+ if (await pathExists(envPath)) {
1582
+ await mergeEnvFile(envPath, moduleEnv, {
1583
+ header: `# --- module: ${module.id} ---`
1584
+ });
1585
+ }
1586
+ }
1523
1587
  }
1524
- }
1525
- const dependencyEntries = selectedModules.reduce(
1526
- (acc, moduleId) => {
1588
+ const dependencyEntries = selectedModules.reduce((acc, moduleId) => {
1527
1589
  const module = getModuleById(moduleId);
1590
+ const emailDependencies = moduleId === "email" && emailProvider ? getEmailProviderDependencies(emailProvider) : {};
1528
1591
  return {
1529
1592
  ...acc,
1593
+ ...emailDependencies,
1530
1594
  ...module.dependencies ?? {}
1531
1595
  };
1532
- },
1533
- {}
1534
- );
1535
- if (Object.keys(dependencyEntries).length > 0) {
1536
- await addDependencies(
1537
- path7.join(cwd, "package.json"),
1538
- dependencyEntries
1539
- );
1540
- }
1541
- const state = await detectProjectState(cwd);
1542
- const namespaceSet = new Set(state.namespaces);
1543
- for (const moduleId of selectedModules) {
1544
- const moduleTemplateMessages = path7.join(
1545
- getModuleById(moduleId).templatePath,
1546
- "messages/vi"
1547
- );
1548
- if (!await pathExists(moduleTemplateMessages)) {
1549
- continue;
1596
+ }, {});
1597
+ if (Object.keys(dependencyEntries).length > 0) {
1598
+ await addDependencies(
1599
+ path7.join(cwd, "package.json"),
1600
+ dependencyEntries
1601
+ );
1550
1602
  }
1551
- const files = await readdir4(moduleTemplateMessages, {
1552
- withFileTypes: true
1553
- });
1554
- for (const file of files) {
1555
- if (!file.isFile() || !file.name.endsWith(".json")) {
1603
+ const state = await detectProjectState(cwd);
1604
+ const namespaceSet = new Set(state.namespaces);
1605
+ for (const moduleId of selectedModules) {
1606
+ const moduleTemplateMessages = path7.join(
1607
+ getModuleById(moduleId).templatePath,
1608
+ "messages/vi"
1609
+ );
1610
+ if (!await pathExists(moduleTemplateMessages)) {
1556
1611
  continue;
1557
1612
  }
1558
- const namespace = file.name.replace(/\.json$/, "");
1559
- namespaceSet.add(namespace);
1560
- const templateData = JSON.parse(
1561
- await readFile6(
1562
- path7.join(moduleTemplateMessages, file.name),
1563
- "utf8"
1613
+ const files = await readdir4(moduleTemplateMessages, {
1614
+ withFileTypes: true
1615
+ });
1616
+ for (const file of files) {
1617
+ if (!file.isFile() || !file.name.endsWith(".json")) {
1618
+ continue;
1619
+ }
1620
+ const namespace = file.name.replace(/\.json$/, "");
1621
+ namespaceSet.add(namespace);
1622
+ const templateData = JSON.parse(
1623
+ await readFile6(
1624
+ path7.join(moduleTemplateMessages, file.name),
1625
+ "utf8"
1626
+ )
1627
+ );
1628
+ await writeNamespaceMessages(cwd, namespace, templateData);
1629
+ await appendNamespace(cwd, namespace);
1630
+ }
1631
+ }
1632
+ await writeManifest(cwd, {
1633
+ ...state,
1634
+ namespaces: [...namespaceSet],
1635
+ modules: [
1636
+ ...new Set(
1637
+ [...state.modules, ...selectedModules].map(
1638
+ (moduleId) => moduleId === "resend" ? "email" : moduleId
1639
+ )
1640
+ )
1641
+ ]
1642
+ });
1643
+ const mergedModules = [
1644
+ ...new Set(
1645
+ [...state.modules, ...selectedModules].map(
1646
+ (moduleId) => moduleId === "resend" ? "email" : moduleId
1564
1647
  )
1648
+ )
1649
+ ];
1650
+ await mergeModuleSetupSections(cwd, selectedModules, mergedModules);
1651
+ finishPrompt(`Added modules: ${selectedModules.join(", ")}`);
1652
+ log.detail("Copied files", String(copiedFileCount));
1653
+ if (autoAddedModules.length > 0) {
1654
+ log.detail("Auto-added", autoAddedModules.join(", "));
1655
+ }
1656
+ if (emailProvider) {
1657
+ log.detail("Email provider", emailProvider);
1658
+ }
1659
+ if (skippedConflictsByModule.length > 0) {
1660
+ const totalSkipped = skippedConflictsByModule.reduce(
1661
+ (total, item) => total + item.paths.length,
1662
+ 0
1565
1663
  );
1566
- await writeNamespaceMessages(cwd, namespace, templateData);
1567
- await appendNamespace(cwd, namespace);
1664
+ log.warn(
1665
+ `Skipped ${totalSkipped} existing file(s). Existing project files were kept unchanged.`
1666
+ );
1667
+ for (const conflict of skippedConflictsByModule) {
1668
+ const preview = conflict.paths.slice(0, 3).join(", ");
1669
+ const suffix = conflict.paths.length > 3 ? ", ..." : "";
1670
+ log.detail(`Skipped (${conflict.moduleId})`, `${preview}${suffix}`);
1671
+ }
1568
1672
  }
1569
- }
1570
- await writeManifest(cwd, {
1571
- ...state,
1572
- namespaces: [...namespaceSet],
1573
- modules: [.../* @__PURE__ */ new Set([...state.modules, ...selectedModules])]
1574
- });
1575
- const mergedModules = [
1576
- .../* @__PURE__ */ new Set([...state.modules, ...selectedModules])
1577
- ];
1578
- await mergeModuleSetupSections(cwd, selectedModules, mergedModules);
1579
- finishPrompt(`Added modules: ${selectedModules.join(", ")}`);
1580
- log.detail("Copied files", String(copiedFileCount));
1581
- if (autoAddedModules.length > 0) {
1582
- log.detail("Auto-added", autoAddedModules.join(", "));
1583
- }
1584
- if (skippedConflictsByModule.length > 0) {
1585
- const totalSkipped = skippedConflictsByModule.reduce(
1586
- (total, item) => total + item.paths.length,
1587
- 0
1588
- );
1589
- log.warn(
1590
- `Skipped ${totalSkipped} existing file(s). Existing project files were kept unchanged.`
1591
- );
1592
- for (const conflict of skippedConflictsByModule) {
1593
- const preview = conflict.paths.slice(0, 3).join(", ");
1594
- const suffix = conflict.paths.length > 3 ? ", ..." : "";
1595
- log.detail(`Skipped (${conflict.moduleId})`, `${preview}${suffix}`);
1673
+ if (chatSchemaStatus === "added") {
1674
+ log.info(
1675
+ "Optional chat schema block was appended to prisma/schema.prisma."
1676
+ );
1596
1677
  }
1597
- }
1598
- if (chatSchemaStatus === "added") {
1599
- log.info(
1600
- "Optional chat schema block was appended to prisma/schema.prisma."
1678
+ log.step(
1679
+ "Next: run your package manager install to apply new dependencies."
1601
1680
  );
1602
1681
  }
1603
- log.step(
1604
- "Next: run your package manager install to apply new dependencies."
1605
- );
1606
- });
1682
+ );
1607
1683
  add.command("language").description("Add locales and clone message files from Vietnamese").option("--locale <locale...>", "Preselect locales: en,ja,ko").option("--yes", "Skip prompts").action(async (options) => {
1608
1684
  const cwd = process.cwd();
1609
1685
  const hasMessages = await pathExists(path7.join(cwd, "messages"));
@@ -1735,7 +1811,9 @@ function registerAddCommand(program2) {
1735
1811
  for (const envFile of envTargets) {
1736
1812
  const envPath = path7.join(cwd, envFile);
1737
1813
  if (await pathExists(envPath)) {
1738
- await mergeEnvFile(envPath, envEntries);
1814
+ await mergeEnvFile(envPath, envEntries, {
1815
+ header: "# --- auth providers ---"
1816
+ });
1739
1817
  }
1740
1818
  }
1741
1819
  finishPrompt(`Enabled providers: ${mergedProviders.join(", ")}`);
@@ -1844,6 +1922,25 @@ function registerCreateCommand(program2) {
1844
1922
  []
1845
1923
  );
1846
1924
  const { selectedModules, autoAddedModules } = normalizeModuleSelection2(rawModules);
1925
+ let emailProvider;
1926
+ if (selectedModules.includes("email")) {
1927
+ emailProvider = await askSelect(
1928
+ "Select email provider:",
1929
+ [
1930
+ {
1931
+ value: "resend",
1932
+ label: "Resend",
1933
+ hint: "Transactional email API with React Email support"
1934
+ },
1935
+ {
1936
+ value: "smtp",
1937
+ label: "SMTP",
1938
+ hint: "Use your SMTP relay credentials"
1939
+ }
1940
+ ],
1941
+ "resend"
1942
+ );
1943
+ }
1847
1944
  const shouldInstall = await askConfirm("Install dependencies now?", true);
1848
1945
  await copyDirectory(templatePaths.base, targetPath);
1849
1946
  for (const moduleId of selectedModules) {
@@ -1854,30 +1951,29 @@ function registerCreateCommand(program2) {
1854
1951
  if (selectedModules.includes("chat")) {
1855
1952
  chatSchemaStatus = await ensureChatSchemaInProject(targetPath);
1856
1953
  }
1857
- const moduleEnvEntries = selectedModules.reduce(
1858
- (acc, moduleId) => {
1859
- const module = getModuleById(moduleId);
1860
- return {
1861
- ...acc,
1862
- ...module.env
1863
- };
1864
- },
1865
- {}
1866
- );
1867
- if (Object.keys(moduleEnvEntries).length > 0) {
1868
- const envTargets = [".env", ".env.example", ".env.development"];
1954
+ const envTargets = [".env", ".env.example", ".env.development"];
1955
+ for (const moduleId of selectedModules) {
1956
+ const module = getModuleById(moduleId);
1957
+ const moduleEnv = moduleId === "email" && emailProvider ? getEmailProviderEnv(emailProvider) : module.env;
1958
+ if (Object.keys(moduleEnv).length === 0) {
1959
+ continue;
1960
+ }
1869
1961
  for (const envFile of envTargets) {
1870
1962
  const envPath = path8.join(targetPath, envFile);
1871
1963
  if (await pathExists(envPath)) {
1872
- await mergeEnvFile(envPath, moduleEnvEntries);
1964
+ await mergeEnvFile(envPath, moduleEnv, {
1965
+ header: `# --- module: ${module.id} ---`
1966
+ });
1873
1967
  }
1874
1968
  }
1875
1969
  }
1876
1970
  const dependencyEntries = selectedModules.reduce(
1877
1971
  (acc, moduleId) => {
1878
1972
  const module = getModuleById(moduleId);
1973
+ const emailDependencies = moduleId === "email" && emailProvider ? getEmailProviderDependencies(emailProvider) : {};
1879
1974
  return {
1880
1975
  ...acc,
1976
+ ...emailDependencies,
1881
1977
  ...module.dependencies ?? {}
1882
1978
  };
1883
1979
  },
@@ -1892,9 +1988,8 @@ function registerCreateCommand(program2) {
1892
1988
  const betterAuthSecret = randomBytes(32).toString("base64url");
1893
1989
  await replaceTokensInDirectory(targetPath, {
1894
1990
  __PROJECT_NAME__: projectSlug,
1895
- __ENABLE_CHAT__: selectedModules.includes("chat") ? "true" : "false",
1896
1991
  __BETTER_AUTH_SECRET__: betterAuthSecret,
1897
- __NEXTCLI_VERSION__: "0.4.2"
1992
+ __NEXTCLI_VERSION__: "0.6.0"
1898
1993
  });
1899
1994
  await mergeModuleSetupSections(
1900
1995
  targetPath,
@@ -1905,7 +2000,7 @@ function registerCreateCommand(program2) {
1905
2000
  if (manifest) {
1906
2001
  await writeManifest(targetPath, {
1907
2002
  ...manifest,
1908
- cli: "0.4.2",
2003
+ cli: "0.6.0",
1909
2004
  modules: selectedModules
1910
2005
  });
1911
2006
  }
@@ -1925,6 +2020,9 @@ function registerCreateCommand(program2) {
1925
2020
  if (autoAddedModules.length > 0) {
1926
2021
  log.detail("Auto-added", autoAddedModules.join(", "));
1927
2022
  }
2023
+ if (emailProvider) {
2024
+ log.detail("Email provider", emailProvider);
2025
+ }
1928
2026
  if (chatSchemaStatus === "added") {
1929
2027
  log.info(
1930
2028
  "Optional chat schema block was appended to prisma/schema.prisma."
@@ -2147,7 +2245,7 @@ var NexTCLICommand = class _NexTCLICommand extends Command {
2147
2245
 
2148
2246
  // src/cli.ts
2149
2247
  var program = new NexTCLICommand();
2150
- program.name("nextcli").description("Scaffold outsource-ready Next.js projects").version("0.4.2");
2248
+ program.name("nextcli").description("Scaffold outsource-ready Next.js projects").version("0.6.0");
2151
2249
  registerCreateCommand(program);
2152
2250
  registerAddCommand(program);
2153
2251
  registerMigrateCommand(program);