@thinhnguyencth1204/nextcli 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +6 -2
  2. package/dist/cli.js +216 -75
  3. package/package.json +2 -1
  4. package/templates/next-base/PROJECT_STRUCTURE.md +88 -0
  5. package/templates/next-base/SETUP.md +86 -0
  6. package/templates/next-base/bun.lock +1443 -0
  7. package/templates/next-base/messages/vi/auth.json +18 -4
  8. package/templates/next-base/next-env.d.ts +3 -1
  9. package/templates/next-base/prisma/migrations/20260612000000_init/migration.sql +104 -0
  10. package/templates/next-base/prisma/migrations/migration_lock.toml +3 -0
  11. package/templates/next-base/prisma/schema.prisma +23 -9
  12. package/templates/next-base/public/logo.svg +4 -0
  13. package/templates/next-base/src/app/(auth)/change-password/layout.tsx +21 -0
  14. package/templates/next-base/src/app/(auth)/change-password/page.tsx +14 -0
  15. package/templates/next-base/src/app/(auth)/layout.tsx +3 -3
  16. package/templates/next-base/src/app/(auth)/sign-in/layout.tsx +17 -0
  17. package/templates/next-base/src/app/(dashboard)/layout.tsx +13 -1
  18. package/templates/next-base/src/app/api/v1/auth/change-password/route.ts +55 -0
  19. package/templates/next-base/src/app/api/v1/auth/login/route.ts +15 -5
  20. package/templates/next-base/src/app/api/v1/auth/me/route.ts +17 -19
  21. package/templates/next-base/src/app/api/v1/users/[id]/route.ts +104 -0
  22. package/templates/next-base/src/app/api/v1/users/route.ts +58 -0
  23. package/templates/next-base/src/app/globals.css +4 -0
  24. package/templates/next-base/src/app/layout.tsx +7 -3
  25. package/templates/next-base/src/components/branding/logo.tsx +27 -0
  26. package/templates/next-base/src/components/layout/private/app-sidebar.tsx +3 -4
  27. package/templates/next-base/src/components/layout/private/dashboard-layout.tsx +2 -1
  28. package/templates/next-base/src/config/branding.ts +14 -0
  29. package/templates/next-base/src/features/auth/components/account-panel.tsx +12 -7
  30. package/templates/next-base/src/features/auth/components/change-password-form.tsx +82 -0
  31. package/templates/next-base/src/features/auth/components/sign-in-form.tsx +18 -13
  32. package/templates/next-base/src/features/auth/validations.ts +7 -1
  33. package/templates/next-base/src/features/users/services.ts +132 -0
  34. package/templates/next-base/src/features/users/validations.ts +21 -0
  35. package/templates/next-base/src/instrumentation.ts +14 -0
  36. package/templates/next-base/src/lib/auth-client.ts +2 -2
  37. package/templates/next-base/src/lib/auth.ts +2 -2
  38. package/templates/next-base/src/lib/bootstrap.ts +96 -0
  39. package/templates/next-base/src/lib/constants.ts +7 -0
  40. package/templates/next-base/src/lib/rbac.ts +62 -0
  41. package/templates/next-base/tsconfig.json +29 -7
package/README.md CHANGED
@@ -30,19 +30,23 @@ This command is fully interactive:
30
30
  - multi-select optional modules (`chat`, `supabase`, `supabase-realtime`, `seo`, `resend`)
31
31
  - confirm install step
32
32
  - normalizes project directory name into a safe project slug for generated `package.json` and env placeholders
33
+ - ships `SETUP.md` and `PROJECT_STRUCTURE.md` in the generated project root
33
34
 
34
35
  ## Core auth in base template
35
36
 
36
37
  Generated projects now include:
37
38
 
38
- - Better Auth + Prisma adapter with JWT plugin enabled
39
- - email/password sign-in scaffold (`/sign-in`)
39
+ - Better Auth + Prisma adapter with JWT + username plugins
40
+ - username/password sign-in (`/sign-in`) and forced password change (`/change-password`)
41
+ - default bootstrap user `admin` / `admin` (must change password on first login)
42
+ - hierarchical RBAC (`Role.level`) and user CRUD under `/api/v1/users`
40
43
  - account sample page (`/account`)
41
44
  - auth API wrappers:
42
45
  - `POST /api/v1/auth/login`
43
46
  - `POST /api/v1/auth/refresh`
44
47
  - `POST /api/v1/auth/logout`
45
48
  - `GET /api/v1/auth/me`
49
+ - `POST /api/v1/auth/change-password`
46
50
 
47
51
  Axios setup is split:
48
52
 
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/commands/add.ts
4
- import path6 from "path";
4
+ import path7 from "path";
5
5
 
6
6
  // src/core/fs.ts
7
7
  import {
@@ -14,6 +14,24 @@ import {
14
14
  writeFile
15
15
  } from "fs/promises";
16
16
  import path from "path";
17
+ var TEMPLATE_COPY_SKIP_DIRS = /* @__PURE__ */ new Set([
18
+ "node_modules",
19
+ ".next",
20
+ ".git",
21
+ "dist",
22
+ "out",
23
+ ".turbo",
24
+ "coverage"
25
+ ]);
26
+ function shouldSkipTemplateDir(dirName) {
27
+ return TEMPLATE_COPY_SKIP_DIRS.has(dirName);
28
+ }
29
+ function shouldSkipRelativeTemplatePath(relativePath) {
30
+ if (!relativePath || relativePath === ".") {
31
+ return false;
32
+ }
33
+ return relativePath.split(path.sep).some((segment) => shouldSkipTemplateDir(segment));
34
+ }
17
35
  async function ensureDir(targetPath) {
18
36
  await mkdir(targetPath, { recursive: true });
19
37
  }
@@ -27,7 +45,13 @@ async function pathExists(targetPath) {
27
45
  }
28
46
  async function copyDirectory(source, destination) {
29
47
  await ensureDir(destination);
30
- await cp(source, destination, { recursive: true });
48
+ await cp(source, destination, {
49
+ recursive: true,
50
+ filter: (src) => {
51
+ const relativePath = path.relative(source, src);
52
+ return !shouldSkipRelativeTemplatePath(relativePath);
53
+ }
54
+ });
31
55
  }
32
56
  async function copyDirectorySafely(source, destination) {
33
57
  const report = {
@@ -41,6 +65,9 @@ async function copyDirectorySafely(source, destination) {
41
65
  const sourcePath = path.join(currentSource, entry.name);
42
66
  const destinationPath = path.join(currentDestination, entry.name);
43
67
  if (entry.isDirectory()) {
68
+ if (shouldSkipTemplateDir(entry.name)) {
69
+ continue;
70
+ }
44
71
  await walk(sourcePath, destinationPath);
45
72
  continue;
46
73
  }
@@ -48,7 +75,9 @@ async function copyDirectorySafely(source, destination) {
48
75
  continue;
49
76
  }
50
77
  if (await pathExists(destinationPath)) {
51
- report.skippedConflicts.push(path.relative(destination, destinationPath));
78
+ report.skippedConflicts.push(
79
+ path.relative(destination, destinationPath)
80
+ );
52
81
  continue;
53
82
  }
54
83
  await ensureDir(path.dirname(destinationPath));
@@ -90,7 +119,9 @@ async function replaceTokensInDirectory(directoryPath, replacements) {
90
119
  for (const entry of entries) {
91
120
  const fullPath = path.join(directoryPath, entry.name);
92
121
  if (entry.isDirectory()) {
93
- await replaceTokensInDirectory(fullPath, replacements);
122
+ if (!shouldSkipTemplateDir(entry.name)) {
123
+ await replaceTokensInDirectory(fullPath, replacements);
124
+ }
94
125
  continue;
95
126
  }
96
127
  if (!entry.isFile() || !isTextFile(fullPath)) {
@@ -152,12 +183,16 @@ async function addDependencies(packageJsonPath, dependencies) {
152
183
  ...current,
153
184
  ...dependencies
154
185
  };
155
- await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}
156
- `, "utf8");
186
+ await writeFile(
187
+ packageJsonPath,
188
+ `${JSON.stringify(packageJson, null, 2)}
189
+ `,
190
+ "utf8"
191
+ );
157
192
  }
158
193
 
159
194
  // src/commands/add.ts
160
- import { readdir as readdir4, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
195
+ import { readdir as readdir4, readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
161
196
 
162
197
  // src/core/templates.ts
163
198
  import path2 from "path";
@@ -182,7 +217,12 @@ var optionalModules = [
182
217
  templatePath: templatePaths.chat,
183
218
  env: {
184
219
  NEXT_PUBLIC_ENABLE_CHAT: "true"
185
- }
220
+ },
221
+ setupSection: `| Variable | Where to get |
222
+ | -------- | ------------ |
223
+ | \`NEXT_PUBLIC_ENABLE_CHAT\` | Set \`true\` when chat module is enabled (auto on add) |
224
+
225
+ Requires \`supabase-realtime\` (auto-added). Run \`db:migrate\` after add \u2014 chat Prisma models are appended.`
186
226
  },
187
227
  {
188
228
  id: "supabase",
@@ -196,7 +236,12 @@ var optionalModules = [
196
236
  },
197
237
  dependencies: {
198
238
  "@supabase/supabase-js": "^2.44.2"
199
- }
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\`) |`
200
245
  },
201
246
  {
202
247
  id: "supabase-realtime",
@@ -209,14 +254,16 @@ var optionalModules = [
209
254
  },
210
255
  dependencies: {
211
256
  "@supabase/supabase-js": "^2.44.2"
212
- }
257
+ },
258
+ setupSection: `Uses same Supabase URL/anon key as \`supabase\` module. Enable Realtime on tables in Supabase Dashboard \u2192 Database \u2192 Replication.`
213
259
  },
214
260
  {
215
261
  id: "seo",
216
262
  label: "SEO pack",
217
263
  description: "Adds robots/sitemap and JsonLd helper files",
218
264
  templatePath: templatePaths.seo,
219
- env: {}
265
+ env: {},
266
+ setupSection: `No extra env keys. Edit \`src/app/robots.ts\`, \`sitemap.ts\`, and JSON-LD helpers after add.`
220
267
  },
221
268
  {
222
269
  id: "resend",
@@ -231,7 +278,11 @@ var optionalModules = [
231
278
  resend: "^6.9.2",
232
279
  "@react-email/components": "^1.0.12",
233
280
  "react-email": "^4.0.0"
234
- }
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>\` |`
235
286
  }
236
287
  ];
237
288
  function getModuleById(moduleId) {
@@ -509,7 +560,7 @@ import { readdir as readdir3, readFile as readFile4, writeFile as writeFile4 } f
509
560
  import path4 from "path";
510
561
  import { readdir as readdir2, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
511
562
  var defaultManifest = {
512
- cli: "0.3.0",
563
+ cli: "0.4.1",
513
564
  defaultLocale: "vi",
514
565
  locales: ["vi"],
515
566
  namespaces: ["common", "auth", "example"],
@@ -679,6 +730,85 @@ async function writeNamespaceMessages(projectDir, namespace, viTemplate) {
679
730
  }
680
731
  }
681
732
 
733
+ // src/core/setup-docs.ts
734
+ import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
735
+ import path6 from "path";
736
+ var ENABLED_MODULES_START = "<!-- nextcli:enabled-modules:start -->";
737
+ var ENABLED_MODULES_END = "<!-- nextcli:enabled-modules:end -->";
738
+ var MODULE_ENV_START = "<!-- nextcli:module-env:start -->";
739
+ var MODULE_ENV_END = "<!-- nextcli:module-env:end -->";
740
+ function buildModuleSection(moduleId) {
741
+ const module = getModuleById(moduleId);
742
+ if (!module.setupSection) {
743
+ return null;
744
+ }
745
+ return `### Module: ${module.label} (\`${module.id}\`)
746
+
747
+ ${module.setupSection.trim()}
748
+ `;
749
+ }
750
+ function formatEnabledModulesLine(moduleIds) {
751
+ if (moduleIds.length === 0) {
752
+ return "**Enabled modules:** none";
753
+ }
754
+ const labels = moduleIds.map((id) => `\`${id}\``).join(", ");
755
+ return `**Enabled modules:** ${labels}`;
756
+ }
757
+ async function updateEnabledModulesLine(content, moduleIds) {
758
+ const startIndex = content.indexOf(ENABLED_MODULES_START);
759
+ const endIndex = content.indexOf(ENABLED_MODULES_END);
760
+ if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
761
+ return content;
762
+ }
763
+ const before = content.slice(0, startIndex + ENABLED_MODULES_START.length);
764
+ const after = content.slice(endIndex);
765
+ const line = formatEnabledModulesLine(moduleIds);
766
+ return `${before}
767
+ ${line}
768
+ ${after}`;
769
+ }
770
+ async function mergeModuleSetupSections(projectDir, moduleIds, allProjectModules) {
771
+ const setupPath = path6.join(projectDir, "SETUP.md");
772
+ if (!await pathExists(setupPath)) {
773
+ return;
774
+ }
775
+ let content = await readFile5(setupPath, "utf8");
776
+ const enabledIds = allProjectModules ?? moduleIds;
777
+ content = await updateEnabledModulesLine(content, enabledIds);
778
+ const sections = moduleIds.map((moduleId) => buildModuleSection(moduleId)).filter((section) => Boolean(section));
779
+ if (sections.length === 0) {
780
+ await writeFile5(setupPath, content, "utf8");
781
+ return;
782
+ }
783
+ const startIndex = content.indexOf(MODULE_ENV_START);
784
+ const endIndex = content.indexOf(MODULE_ENV_END);
785
+ if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
786
+ await writeFile5(setupPath, content, "utf8");
787
+ return;
788
+ }
789
+ const before = content.slice(0, startIndex + MODULE_ENV_START.length);
790
+ const after = content.slice(endIndex);
791
+ const existingBlock = content.slice(
792
+ startIndex + MODULE_ENV_START.length,
793
+ endIndex
794
+ );
795
+ let nextBlock = existingBlock.trim();
796
+ for (const section of sections) {
797
+ const header = section.split("\n")[0];
798
+ if (header && nextBlock.includes(header)) {
799
+ continue;
800
+ }
801
+ nextBlock = nextBlock ? `${nextBlock}
802
+
803
+ ${section}` : section;
804
+ }
805
+ content = `${before}
806
+ ${nextBlock ? `
807
+ ${nextBlock}
808
+ ` : "\n"}${after}`;
809
+ await writeFile5(setupPath, content, "utf8");
810
+ }
811
+
682
812
  // src/commands/add.ts
683
813
  function toKebabCase(input) {
684
814
  return input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -1094,11 +1224,11 @@ export async function DELETE(
1094
1224
  `;
1095
1225
  }
1096
1226
  async function appendFeatureModelToPrismaSchema(cwd, modelPascal) {
1097
- const schemaPath = path6.join(cwd, "prisma", "schema.prisma");
1227
+ const schemaPath = path7.join(cwd, "prisma", "schema.prisma");
1098
1228
  if (!await pathExists(schemaPath)) {
1099
1229
  return "skipped";
1100
1230
  }
1101
- const schemaContent = await readFile5(schemaPath, "utf8");
1231
+ const schemaContent = await readFile6(schemaPath, "utf8");
1102
1232
  const modelRegex = new RegExp(`\\bmodel\\s+${modelPascal}\\b`);
1103
1233
  if (modelRegex.test(schemaContent)) {
1104
1234
  return "exists";
@@ -1115,7 +1245,7 @@ model ${modelPascal} {
1115
1245
  updatedAt DateTime @updatedAt
1116
1246
  }
1117
1247
  `;
1118
- await writeFile5(
1248
+ await writeFile6(
1119
1249
  schemaPath,
1120
1250
  `${schemaContent.trimEnd()}${modelBlock}
1121
1251
  `,
@@ -1181,18 +1311,18 @@ async function upsertEnvValue(envFilePath, key, value) {
1181
1311
  if (!await pathExists(envFilePath)) {
1182
1312
  return;
1183
1313
  }
1184
- const content = await readFile5(envFilePath, "utf8");
1314
+ const content = await readFile6(envFilePath, "utf8");
1185
1315
  const entry = `${key}=${value}`;
1186
1316
  const pattern = new RegExp(`^${key}=.*$`, "m");
1187
1317
  if (pattern.test(content)) {
1188
1318
  const next = content.replace(pattern, entry);
1189
1319
  if (next !== content) {
1190
- await writeFile5(envFilePath, next, "utf8");
1320
+ await writeFile6(envFilePath, next, "utf8");
1191
1321
  }
1192
1322
  return;
1193
1323
  }
1194
1324
  const separator = content.endsWith("\n") || content.length === 0 ? "" : "\n";
1195
- await writeFile5(envFilePath, `${content}${separator}${entry}
1325
+ await writeFile6(envFilePath, `${content}${separator}${entry}
1196
1326
  `, "utf8");
1197
1327
  }
1198
1328
  function registerAddCommand(program2) {
@@ -1205,7 +1335,7 @@ function registerAddCommand(program2) {
1205
1335
  return;
1206
1336
  }
1207
1337
  const cwd = process.cwd();
1208
- const srcPath = path6.join(cwd, "src");
1338
+ const srcPath = path7.join(cwd, "src");
1209
1339
  if (!await pathExists(srcPath)) {
1210
1340
  log.error(
1211
1341
  "Run this command from your generated Next.js project root (missing ./src)."
@@ -1216,36 +1346,36 @@ function registerAddCommand(program2) {
1216
1346
  const featurePascal = toPascalCase(featureSlug);
1217
1347
  const modelPascal = singularizeWord(featurePascal);
1218
1348
  const modelDelegate = toCamelCase(modelPascal);
1219
- const featureRoot = path6.join(cwd, "src/features", featureSlug);
1349
+ const featureRoot = path7.join(cwd, "src/features", featureSlug);
1220
1350
  if (await pathExists(featureRoot)) {
1221
1351
  log.error(`Feature already exists: ${featureRoot}`);
1222
1352
  process.exitCode = 1;
1223
1353
  return;
1224
1354
  }
1225
- await ensureDir(path6.join(featureRoot, "api"));
1226
- await ensureDir(path6.join(featureRoot, "components"));
1227
- await writeFile5(
1228
- path6.join(featureRoot, "services.ts"),
1355
+ await ensureDir(path7.join(featureRoot, "api"));
1356
+ await ensureDir(path7.join(featureRoot, "components"));
1357
+ await writeFile6(
1358
+ path7.join(featureRoot, "services.ts"),
1229
1359
  buildFeatureServicesContent(modelPascal, modelDelegate),
1230
1360
  "utf8"
1231
1361
  );
1232
- await writeFile5(
1233
- path6.join(featureRoot, "validations.ts"),
1362
+ await writeFile6(
1363
+ path7.join(featureRoot, "validations.ts"),
1234
1364
  buildFeatureValidationContent(modelPascal),
1235
1365
  "utf8"
1236
1366
  );
1237
- await writeFile5(
1238
- path6.join(featureRoot, "api", `use-${featureSlug}.ts`),
1367
+ await writeFile6(
1368
+ path7.join(featureRoot, "api", `use-${featureSlug}.ts`),
1239
1369
  buildFeatureHooksContent(featureSlug, modelPascal),
1240
1370
  "utf8"
1241
1371
  );
1242
- await writeFile5(
1243
- path6.join(featureRoot, "components", `${featureSlug}-table.tsx`),
1372
+ await writeFile6(
1373
+ path7.join(featureRoot, "components", `${featureSlug}-table.tsx`),
1244
1374
  buildFeatureTableContent(featureSlug, modelPascal),
1245
1375
  "utf8"
1246
1376
  );
1247
- await writeFile5(
1248
- path6.join(
1377
+ await writeFile6(
1378
+ path7.join(
1249
1379
  featureRoot,
1250
1380
  "components",
1251
1381
  `create-${featureSlug}-dialog.tsx`
@@ -1253,39 +1383,39 @@ function registerAddCommand(program2) {
1253
1383
  buildFeatureDialogContent(featureSlug, modelPascal),
1254
1384
  "utf8"
1255
1385
  );
1256
- const routeFilePath = path6.join(
1386
+ const routeFilePath = path7.join(
1257
1387
  cwd,
1258
1388
  "src/app/api/v1",
1259
1389
  featureSlug,
1260
1390
  "route.ts"
1261
1391
  );
1262
- await ensureDir(path6.dirname(routeFilePath));
1263
- await writeFile5(
1392
+ await ensureDir(path7.dirname(routeFilePath));
1393
+ await writeFile6(
1264
1394
  routeFilePath,
1265
1395
  buildCollectionRouteContent(featureSlug, modelPascal),
1266
1396
  "utf8"
1267
1397
  );
1268
- const idRoutePath = path6.join(
1398
+ const idRoutePath = path7.join(
1269
1399
  cwd,
1270
1400
  "src/app/api/v1",
1271
1401
  featureSlug,
1272
1402
  "[id]",
1273
1403
  "route.ts"
1274
1404
  );
1275
- await ensureDir(path6.dirname(idRoutePath));
1276
- await writeFile5(
1405
+ await ensureDir(path7.dirname(idRoutePath));
1406
+ await writeFile6(
1277
1407
  idRoutePath,
1278
1408
  buildItemRouteContent(featureSlug, modelPascal),
1279
1409
  "utf8"
1280
1410
  );
1281
- const featurePagePath = path6.join(
1411
+ const featurePagePath = path7.join(
1282
1412
  cwd,
1283
1413
  "src/app/(dashboard)",
1284
1414
  featureSlug,
1285
1415
  "page.tsx"
1286
1416
  );
1287
- await ensureDir(path6.dirname(featurePagePath));
1288
- await writeFile5(
1417
+ await ensureDir(path7.dirname(featurePagePath));
1418
+ await writeFile6(
1289
1419
  featurePagePath,
1290
1420
  buildFeaturePageContent(featureSlug, modelPascal),
1291
1421
  "utf8"
@@ -1315,8 +1445,8 @@ function registerAddCommand(program2) {
1315
1445
  });
1316
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) => {
1317
1447
  const cwd = process.cwd();
1318
- const hasSrc = await pathExists(path6.join(cwd, "src"));
1319
- const hasPackageJson = await pathExists(path6.join(cwd, "package.json"));
1448
+ const hasSrc = await pathExists(path7.join(cwd, "src"));
1449
+ const hasPackageJson = await pathExists(path7.join(cwd, "package.json"));
1320
1450
  if (!hasSrc || !hasPackageJson) {
1321
1451
  log.error("Run this command from your generated Next.js project root.");
1322
1452
  process.exitCode = 1;
@@ -1376,7 +1506,7 @@ function registerAddCommand(program2) {
1376
1506
  if (Object.keys(envEntries).length > 0) {
1377
1507
  const envTargets = [".env", ".env.example", ".env.development"];
1378
1508
  for (const envFile of envTargets) {
1379
- const envPath = path6.join(cwd, envFile);
1509
+ const envPath = path7.join(cwd, envFile);
1380
1510
  if (await pathExists(envPath)) {
1381
1511
  await mergeEnvFile(envPath, envEntries);
1382
1512
  }
@@ -1386,7 +1516,7 @@ function registerAddCommand(program2) {
1386
1516
  const envTargets = [".env", ".env.example", ".env.development"];
1387
1517
  for (const envFile of envTargets) {
1388
1518
  await upsertEnvValue(
1389
- path6.join(cwd, envFile),
1519
+ path7.join(cwd, envFile),
1390
1520
  "NEXT_PUBLIC_ENABLE_CHAT",
1391
1521
  "true"
1392
1522
  );
@@ -1404,14 +1534,14 @@ function registerAddCommand(program2) {
1404
1534
  );
1405
1535
  if (Object.keys(dependencyEntries).length > 0) {
1406
1536
  await addDependencies(
1407
- path6.join(cwd, "package.json"),
1537
+ path7.join(cwd, "package.json"),
1408
1538
  dependencyEntries
1409
1539
  );
1410
1540
  }
1411
1541
  const state = await detectProjectState(cwd);
1412
1542
  const namespaceSet = new Set(state.namespaces);
1413
1543
  for (const moduleId of selectedModules) {
1414
- const moduleTemplateMessages = path6.join(
1544
+ const moduleTemplateMessages = path7.join(
1415
1545
  getModuleById(moduleId).templatePath,
1416
1546
  "messages/vi"
1417
1547
  );
@@ -1428,8 +1558,8 @@ function registerAddCommand(program2) {
1428
1558
  const namespace = file.name.replace(/\.json$/, "");
1429
1559
  namespaceSet.add(namespace);
1430
1560
  const templateData = JSON.parse(
1431
- await readFile5(
1432
- path6.join(moduleTemplateMessages, file.name),
1561
+ await readFile6(
1562
+ path7.join(moduleTemplateMessages, file.name),
1433
1563
  "utf8"
1434
1564
  )
1435
1565
  );
@@ -1442,6 +1572,10 @@ function registerAddCommand(program2) {
1442
1572
  namespaces: [...namespaceSet],
1443
1573
  modules: [.../* @__PURE__ */ new Set([...state.modules, ...selectedModules])]
1444
1574
  });
1575
+ const mergedModules = [
1576
+ .../* @__PURE__ */ new Set([...state.modules, ...selectedModules])
1577
+ ];
1578
+ await mergeModuleSetupSections(cwd, selectedModules, mergedModules);
1445
1579
  finishPrompt(`Added modules: ${selectedModules.join(", ")}`);
1446
1580
  log.detail("Copied files", String(copiedFileCount));
1447
1581
  if (autoAddedModules.length > 0) {
@@ -1472,8 +1606,8 @@ function registerAddCommand(program2) {
1472
1606
  });
1473
1607
  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) => {
1474
1608
  const cwd = process.cwd();
1475
- const hasMessages = await pathExists(path6.join(cwd, "messages"));
1476
- const hasConfig = await pathExists(path6.join(cwd, "src/i18n/config.ts"));
1609
+ const hasMessages = await pathExists(path7.join(cwd, "messages"));
1610
+ const hasConfig = await pathExists(path7.join(cwd, "src/i18n/config.ts"));
1477
1611
  if (!hasMessages || !hasConfig) {
1478
1612
  log.error(
1479
1613
  "Run this command from a generated Next.js project with i18n scaffold."
@@ -1540,9 +1674,9 @@ function registerAddCommand(program2) {
1540
1674
  });
1541
1675
  add.command("auth-provider").description("Add social auth providers to existing Better Auth setup").option("--provider <provider...>", "Preselect providers: google,facebook").option("--yes", "Skip prompts").action(async (options) => {
1542
1676
  const cwd = process.cwd();
1543
- const authFilePath = path6.join(cwd, "src/lib/auth.ts");
1544
- const hasSrc = await pathExists(path6.join(cwd, "src"));
1545
- const hasPackageJson = await pathExists(path6.join(cwd, "package.json"));
1677
+ const authFilePath = path7.join(cwd, "src/lib/auth.ts");
1678
+ const hasSrc = await pathExists(path7.join(cwd, "src"));
1679
+ const hasPackageJson = await pathExists(path7.join(cwd, "package.json"));
1546
1680
  const hasAuthFile = await pathExists(authFilePath);
1547
1681
  if (!hasSrc || !hasPackageJson || !hasAuthFile) {
1548
1682
  log.error(
@@ -1581,13 +1715,13 @@ function registerAddCommand(program2) {
1581
1715
  finishPrompt("No auth providers selected.");
1582
1716
  return;
1583
1717
  }
1584
- const authContent = await readFile5(authFilePath, "utf8");
1718
+ const authContent = await readFile6(authFilePath, "utf8");
1585
1719
  const existingProviders = readConfiguredProviders(authContent);
1586
1720
  const mergedProviders = [
1587
1721
  .../* @__PURE__ */ new Set([...existingProviders, ...selectedProviders])
1588
1722
  ];
1589
1723
  const nextAuthContent = patchAuthProviders(authContent, mergedProviders);
1590
- await writeFile5(authFilePath, nextAuthContent, "utf8");
1724
+ await writeFile6(authFilePath, nextAuthContent, "utf8");
1591
1725
  const envEntries = {};
1592
1726
  if (mergedProviders.includes("google")) {
1593
1727
  envEntries.GOOGLE_CLIENT_ID = "";
@@ -1599,7 +1733,7 @@ function registerAddCommand(program2) {
1599
1733
  }
1600
1734
  const envTargets = [".env", ".env.example", ".env.development"];
1601
1735
  for (const envFile of envTargets) {
1602
- const envPath = path6.join(cwd, envFile);
1736
+ const envPath = path7.join(cwd, envFile);
1603
1737
  if (await pathExists(envPath)) {
1604
1738
  await mergeEnvFile(envPath, envEntries);
1605
1739
  }
@@ -1615,7 +1749,7 @@ function registerAddCommand(program2) {
1615
1749
  // src/commands/create.ts
1616
1750
  import { spawn as spawn2 } from "child_process";
1617
1751
  import { randomBytes } from "crypto";
1618
- import path7 from "path";
1752
+ import path8 from "path";
1619
1753
  async function runInstall(packageManager, cwd) {
1620
1754
  const installArgsMap = {
1621
1755
  npm: ["install"],
@@ -1671,7 +1805,7 @@ async function resolveProjectName() {
1671
1805
  }
1672
1806
  }
1673
1807
  });
1674
- const targetPath = path7.resolve(process.cwd(), projectName);
1808
+ const targetPath = path8.resolve(process.cwd(), projectName);
1675
1809
  if (await pathExists(targetPath)) {
1676
1810
  log.error(`Target directory already exists: ${targetPath}`);
1677
1811
  continue;
@@ -1683,8 +1817,8 @@ function registerCreateCommand(program2) {
1683
1817
  program2.command("create").description("Create a new outsource-ready Next.js app").action(async () => {
1684
1818
  startPrompt("NexTCLI project creation");
1685
1819
  const projectName = await resolveProjectName();
1686
- const targetPath = path7.resolve(process.cwd(), projectName);
1687
- const projectDirectoryName = path7.basename(targetPath);
1820
+ const targetPath = path8.resolve(process.cwd(), projectName);
1821
+ const projectDirectoryName = path8.basename(targetPath);
1688
1822
  const projectSlug = toProjectSlug(projectDirectoryName);
1689
1823
  const packageManager = await askSelect(
1690
1824
  "Which package manager do you want to use?",
@@ -1733,7 +1867,7 @@ function registerCreateCommand(program2) {
1733
1867
  if (Object.keys(moduleEnvEntries).length > 0) {
1734
1868
  const envTargets = [".env", ".env.example", ".env.development"];
1735
1869
  for (const envFile of envTargets) {
1736
- const envPath = path7.join(targetPath, envFile);
1870
+ const envPath = path8.join(targetPath, envFile);
1737
1871
  if (await pathExists(envPath)) {
1738
1872
  await mergeEnvFile(envPath, moduleEnvEntries);
1739
1873
  }
@@ -1751,7 +1885,7 @@ function registerCreateCommand(program2) {
1751
1885
  );
1752
1886
  if (Object.keys(dependencyEntries).length > 0) {
1753
1887
  await addDependencies(
1754
- path7.join(targetPath, "package.json"),
1888
+ path8.join(targetPath, "package.json"),
1755
1889
  dependencyEntries
1756
1890
  );
1757
1891
  }
@@ -1760,13 +1894,18 @@ function registerCreateCommand(program2) {
1760
1894
  __PROJECT_NAME__: projectSlug,
1761
1895
  __ENABLE_CHAT__: selectedModules.includes("chat") ? "true" : "false",
1762
1896
  __BETTER_AUTH_SECRET__: betterAuthSecret,
1763
- __NEXTCLI_VERSION__: "0.3.0"
1897
+ __NEXTCLI_VERSION__: "0.4.1"
1764
1898
  });
1899
+ await mergeModuleSetupSections(
1900
+ targetPath,
1901
+ selectedModules,
1902
+ selectedModules
1903
+ );
1765
1904
  const manifest = await readManifest(targetPath);
1766
1905
  if (manifest) {
1767
1906
  await writeManifest(targetPath, {
1768
1907
  ...manifest,
1769
- cli: "0.3.0",
1908
+ cli: "0.4.1",
1770
1909
  modules: selectedModules
1771
1910
  });
1772
1911
  }
@@ -1791,13 +1930,15 @@ function registerCreateCommand(program2) {
1791
1930
  "Optional chat schema block was appended to prisma/schema.prisma."
1792
1931
  );
1793
1932
  }
1794
- log.step(`Next: cd ${projectName} && ${packageManager} run dev`);
1933
+ log.step(
1934
+ `Next: cd ${projectName} && ${packageManager} run db:migrate && ${packageManager} run dev`
1935
+ );
1795
1936
  });
1796
1937
  }
1797
1938
 
1798
1939
  // src/commands/migrate.ts
1799
1940
  import { spawn as spawn3 } from "child_process";
1800
- import path8 from "path";
1941
+ import path9 from "path";
1801
1942
  function createDefaultMigrationName() {
1802
1943
  const now = /* @__PURE__ */ new Date();
1803
1944
  const y = now.getFullYear();
@@ -1809,16 +1950,16 @@ function createDefaultMigrationName() {
1809
1950
  return `auto_${y}${m}${d}${hh}${mm}${ss}`;
1810
1951
  }
1811
1952
  async function detectPackageManager(cwd) {
1812
- if (await pathExists(path8.join(cwd, "bun.lockb"))) {
1953
+ if (await pathExists(path9.join(cwd, "bun.lockb"))) {
1813
1954
  return "bun";
1814
1955
  }
1815
- if (await pathExists(path8.join(cwd, "bun.lock"))) {
1956
+ if (await pathExists(path9.join(cwd, "bun.lock"))) {
1816
1957
  return "bun";
1817
1958
  }
1818
- if (await pathExists(path8.join(cwd, "pnpm-lock.yaml"))) {
1959
+ if (await pathExists(path9.join(cwd, "pnpm-lock.yaml"))) {
1819
1960
  return "pnpm";
1820
1961
  }
1821
- if (await pathExists(path8.join(cwd, "yarn.lock"))) {
1962
+ if (await pathExists(path9.join(cwd, "yarn.lock"))) {
1822
1963
  return "yarn";
1823
1964
  }
1824
1965
  return "npm";
@@ -1853,8 +1994,8 @@ async function runCommand2(command, args, cwd) {
1853
1994
  function registerMigrateCommand(program2) {
1854
1995
  program2.command("migrate").description("Run Prisma migration script in current project").option("--name <migration-name>", "Migration name (defaults to auto timestamp)").option("--skip-generate", "Pass --skip-generate to prisma migrate dev").action(async (options) => {
1855
1996
  const cwd = process.cwd();
1856
- const hasPackageJson = await pathExists(path8.join(cwd, "package.json"));
1857
- const hasPrismaSchema = await pathExists(path8.join(cwd, "prisma", "schema.prisma"));
1997
+ const hasPackageJson = await pathExists(path9.join(cwd, "package.json"));
1998
+ const hasPrismaSchema = await pathExists(path9.join(cwd, "prisma", "schema.prisma"));
1858
1999
  if (!hasPackageJson || !hasPrismaSchema) {
1859
2000
  log.error(
1860
2001
  "Run this command from a generated project root (requires package.json + prisma/schema.prisma)."
@@ -2006,7 +2147,7 @@ var NexTCLICommand = class _NexTCLICommand extends Command {
2006
2147
 
2007
2148
  // src/cli.ts
2008
2149
  var program = new NexTCLICommand();
2009
- program.name("nextcli").description("Scaffold outsource-ready Next.js projects").version("0.3.0");
2150
+ program.name("nextcli").description("Scaffold outsource-ready Next.js projects").version("0.4.1");
2010
2151
  registerCreateCommand(program);
2011
2152
  registerAddCommand(program);
2012
2153
  registerMigrateCommand(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thinhnguyencth1204/nextcli",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "CLI scaffolder for outsourced Next.js projects",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,6 +33,7 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@clack/prompts": "^0.7.0",
36
+ "@thinhnguyencth1204/nextcli": "^0.4.1",
36
37
  "commander": "^12.1.0"
37
38
  },
38
39
  "devDependencies": {