@thinhnguyencth1204/nextcli 0.3.0 → 0.4.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.
Files changed (41) hide show
  1. package/README.md +6 -2
  2. package/dist/cli.js +210 -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,21 @@ 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 shouldSkipTemplatePath(filePath) {
30
+ return filePath.split(path.sep).some((segment) => shouldSkipTemplateDir(segment));
31
+ }
17
32
  async function ensureDir(targetPath) {
18
33
  await mkdir(targetPath, { recursive: true });
19
34
  }
@@ -27,7 +42,10 @@ async function pathExists(targetPath) {
27
42
  }
28
43
  async function copyDirectory(source, destination) {
29
44
  await ensureDir(destination);
30
- await cp(source, destination, { recursive: true });
45
+ await cp(source, destination, {
46
+ recursive: true,
47
+ filter: (src) => !shouldSkipTemplatePath(src)
48
+ });
31
49
  }
32
50
  async function copyDirectorySafely(source, destination) {
33
51
  const report = {
@@ -41,6 +59,9 @@ async function copyDirectorySafely(source, destination) {
41
59
  const sourcePath = path.join(currentSource, entry.name);
42
60
  const destinationPath = path.join(currentDestination, entry.name);
43
61
  if (entry.isDirectory()) {
62
+ if (shouldSkipTemplateDir(entry.name)) {
63
+ continue;
64
+ }
44
65
  await walk(sourcePath, destinationPath);
45
66
  continue;
46
67
  }
@@ -48,7 +69,9 @@ async function copyDirectorySafely(source, destination) {
48
69
  continue;
49
70
  }
50
71
  if (await pathExists(destinationPath)) {
51
- report.skippedConflicts.push(path.relative(destination, destinationPath));
72
+ report.skippedConflicts.push(
73
+ path.relative(destination, destinationPath)
74
+ );
52
75
  continue;
53
76
  }
54
77
  await ensureDir(path.dirname(destinationPath));
@@ -90,7 +113,9 @@ async function replaceTokensInDirectory(directoryPath, replacements) {
90
113
  for (const entry of entries) {
91
114
  const fullPath = path.join(directoryPath, entry.name);
92
115
  if (entry.isDirectory()) {
93
- await replaceTokensInDirectory(fullPath, replacements);
116
+ if (!shouldSkipTemplateDir(entry.name)) {
117
+ await replaceTokensInDirectory(fullPath, replacements);
118
+ }
94
119
  continue;
95
120
  }
96
121
  if (!entry.isFile() || !isTextFile(fullPath)) {
@@ -152,12 +177,16 @@ async function addDependencies(packageJsonPath, dependencies) {
152
177
  ...current,
153
178
  ...dependencies
154
179
  };
155
- await writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}
156
- `, "utf8");
180
+ await writeFile(
181
+ packageJsonPath,
182
+ `${JSON.stringify(packageJson, null, 2)}
183
+ `,
184
+ "utf8"
185
+ );
157
186
  }
158
187
 
159
188
  // src/commands/add.ts
160
- import { readdir as readdir4, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
189
+ import { readdir as readdir4, readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
161
190
 
162
191
  // src/core/templates.ts
163
192
  import path2 from "path";
@@ -182,7 +211,12 @@ var optionalModules = [
182
211
  templatePath: templatePaths.chat,
183
212
  env: {
184
213
  NEXT_PUBLIC_ENABLE_CHAT: "true"
185
- }
214
+ },
215
+ setupSection: `| Variable | Where to get |
216
+ | -------- | ------------ |
217
+ | \`NEXT_PUBLIC_ENABLE_CHAT\` | Set \`true\` when chat module is enabled (auto on add) |
218
+
219
+ Requires \`supabase-realtime\` (auto-added). Run \`db:migrate\` after add \u2014 chat Prisma models are appended.`
186
220
  },
187
221
  {
188
222
  id: "supabase",
@@ -196,7 +230,12 @@ var optionalModules = [
196
230
  },
197
231
  dependencies: {
198
232
  "@supabase/supabase-js": "^2.44.2"
199
- }
233
+ },
234
+ setupSection: `| Variable | Where to get |
235
+ | -------- | ------------ |
236
+ | \`NEXT_PUBLIC_SUPABASE_URL\` | Supabase Dashboard \u2192 Project Settings \u2192 API \u2192 Project URL |
237
+ | \`NEXT_PUBLIC_SUPABASE_ANON_KEY\` | Same page \u2192 Project API keys \u2192 \`anon\` \`public\` |
238
+ | \`NEXT_PUBLIC_SUPABASE_STORAGE_BUCKET\` | Storage \u2192 create bucket \u2192 use bucket name (default scaffold: \`public\`) |`
200
239
  },
201
240
  {
202
241
  id: "supabase-realtime",
@@ -209,14 +248,16 @@ var optionalModules = [
209
248
  },
210
249
  dependencies: {
211
250
  "@supabase/supabase-js": "^2.44.2"
212
- }
251
+ },
252
+ setupSection: `Uses same Supabase URL/anon key as \`supabase\` module. Enable Realtime on tables in Supabase Dashboard \u2192 Database \u2192 Replication.`
213
253
  },
214
254
  {
215
255
  id: "seo",
216
256
  label: "SEO pack",
217
257
  description: "Adds robots/sitemap and JsonLd helper files",
218
258
  templatePath: templatePaths.seo,
219
- env: {}
259
+ env: {},
260
+ setupSection: `No extra env keys. Edit \`src/app/robots.ts\`, \`sitemap.ts\`, and JSON-LD helpers after add.`
220
261
  },
221
262
  {
222
263
  id: "resend",
@@ -231,7 +272,11 @@ var optionalModules = [
231
272
  resend: "^6.9.2",
232
273
  "@react-email/components": "^1.0.12",
233
274
  "react-email": "^4.0.0"
234
- }
275
+ },
276
+ setupSection: `| Variable | Where to get |
277
+ | -------- | ------------ |
278
+ | \`RESEND_API_KEY\` | resend.com \u2192 API Keys \u2192 Create |
279
+ | \`RESEND_FROM_EMAIL\` | resend.com \u2192 Domains \u2192 verify domain \u2192 use \`Name <you@domain.com>\` |`
235
280
  }
236
281
  ];
237
282
  function getModuleById(moduleId) {
@@ -509,7 +554,7 @@ import { readdir as readdir3, readFile as readFile4, writeFile as writeFile4 } f
509
554
  import path4 from "path";
510
555
  import { readdir as readdir2, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
511
556
  var defaultManifest = {
512
- cli: "0.3.0",
557
+ cli: "0.4.0",
513
558
  defaultLocale: "vi",
514
559
  locales: ["vi"],
515
560
  namespaces: ["common", "auth", "example"],
@@ -679,6 +724,85 @@ async function writeNamespaceMessages(projectDir, namespace, viTemplate) {
679
724
  }
680
725
  }
681
726
 
727
+ // src/core/setup-docs.ts
728
+ import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
729
+ import path6 from "path";
730
+ var ENABLED_MODULES_START = "<!-- nextcli:enabled-modules:start -->";
731
+ var ENABLED_MODULES_END = "<!-- nextcli:enabled-modules:end -->";
732
+ var MODULE_ENV_START = "<!-- nextcli:module-env:start -->";
733
+ var MODULE_ENV_END = "<!-- nextcli:module-env:end -->";
734
+ function buildModuleSection(moduleId) {
735
+ const module = getModuleById(moduleId);
736
+ if (!module.setupSection) {
737
+ return null;
738
+ }
739
+ return `### Module: ${module.label} (\`${module.id}\`)
740
+
741
+ ${module.setupSection.trim()}
742
+ `;
743
+ }
744
+ function formatEnabledModulesLine(moduleIds) {
745
+ if (moduleIds.length === 0) {
746
+ return "**Enabled modules:** none";
747
+ }
748
+ const labels = moduleIds.map((id) => `\`${id}\``).join(", ");
749
+ return `**Enabled modules:** ${labels}`;
750
+ }
751
+ async function updateEnabledModulesLine(content, moduleIds) {
752
+ const startIndex = content.indexOf(ENABLED_MODULES_START);
753
+ const endIndex = content.indexOf(ENABLED_MODULES_END);
754
+ if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
755
+ return content;
756
+ }
757
+ const before = content.slice(0, startIndex + ENABLED_MODULES_START.length);
758
+ const after = content.slice(endIndex);
759
+ const line = formatEnabledModulesLine(moduleIds);
760
+ return `${before}
761
+ ${line}
762
+ ${after}`;
763
+ }
764
+ async function mergeModuleSetupSections(projectDir, moduleIds, allProjectModules) {
765
+ const setupPath = path6.join(projectDir, "SETUP.md");
766
+ if (!await pathExists(setupPath)) {
767
+ return;
768
+ }
769
+ let content = await readFile5(setupPath, "utf8");
770
+ const enabledIds = allProjectModules ?? moduleIds;
771
+ content = await updateEnabledModulesLine(content, enabledIds);
772
+ const sections = moduleIds.map((moduleId) => buildModuleSection(moduleId)).filter((section) => Boolean(section));
773
+ if (sections.length === 0) {
774
+ await writeFile5(setupPath, content, "utf8");
775
+ return;
776
+ }
777
+ const startIndex = content.indexOf(MODULE_ENV_START);
778
+ const endIndex = content.indexOf(MODULE_ENV_END);
779
+ if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
780
+ await writeFile5(setupPath, content, "utf8");
781
+ return;
782
+ }
783
+ const before = content.slice(0, startIndex + MODULE_ENV_START.length);
784
+ const after = content.slice(endIndex);
785
+ const existingBlock = content.slice(
786
+ startIndex + MODULE_ENV_START.length,
787
+ endIndex
788
+ );
789
+ let nextBlock = existingBlock.trim();
790
+ for (const section of sections) {
791
+ const header = section.split("\n")[0];
792
+ if (header && nextBlock.includes(header)) {
793
+ continue;
794
+ }
795
+ nextBlock = nextBlock ? `${nextBlock}
796
+
797
+ ${section}` : section;
798
+ }
799
+ content = `${before}
800
+ ${nextBlock ? `
801
+ ${nextBlock}
802
+ ` : "\n"}${after}`;
803
+ await writeFile5(setupPath, content, "utf8");
804
+ }
805
+
682
806
  // src/commands/add.ts
683
807
  function toKebabCase(input) {
684
808
  return input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -1094,11 +1218,11 @@ export async function DELETE(
1094
1218
  `;
1095
1219
  }
1096
1220
  async function appendFeatureModelToPrismaSchema(cwd, modelPascal) {
1097
- const schemaPath = path6.join(cwd, "prisma", "schema.prisma");
1221
+ const schemaPath = path7.join(cwd, "prisma", "schema.prisma");
1098
1222
  if (!await pathExists(schemaPath)) {
1099
1223
  return "skipped";
1100
1224
  }
1101
- const schemaContent = await readFile5(schemaPath, "utf8");
1225
+ const schemaContent = await readFile6(schemaPath, "utf8");
1102
1226
  const modelRegex = new RegExp(`\\bmodel\\s+${modelPascal}\\b`);
1103
1227
  if (modelRegex.test(schemaContent)) {
1104
1228
  return "exists";
@@ -1115,7 +1239,7 @@ model ${modelPascal} {
1115
1239
  updatedAt DateTime @updatedAt
1116
1240
  }
1117
1241
  `;
1118
- await writeFile5(
1242
+ await writeFile6(
1119
1243
  schemaPath,
1120
1244
  `${schemaContent.trimEnd()}${modelBlock}
1121
1245
  `,
@@ -1181,18 +1305,18 @@ async function upsertEnvValue(envFilePath, key, value) {
1181
1305
  if (!await pathExists(envFilePath)) {
1182
1306
  return;
1183
1307
  }
1184
- const content = await readFile5(envFilePath, "utf8");
1308
+ const content = await readFile6(envFilePath, "utf8");
1185
1309
  const entry = `${key}=${value}`;
1186
1310
  const pattern = new RegExp(`^${key}=.*$`, "m");
1187
1311
  if (pattern.test(content)) {
1188
1312
  const next = content.replace(pattern, entry);
1189
1313
  if (next !== content) {
1190
- await writeFile5(envFilePath, next, "utf8");
1314
+ await writeFile6(envFilePath, next, "utf8");
1191
1315
  }
1192
1316
  return;
1193
1317
  }
1194
1318
  const separator = content.endsWith("\n") || content.length === 0 ? "" : "\n";
1195
- await writeFile5(envFilePath, `${content}${separator}${entry}
1319
+ await writeFile6(envFilePath, `${content}${separator}${entry}
1196
1320
  `, "utf8");
1197
1321
  }
1198
1322
  function registerAddCommand(program2) {
@@ -1205,7 +1329,7 @@ function registerAddCommand(program2) {
1205
1329
  return;
1206
1330
  }
1207
1331
  const cwd = process.cwd();
1208
- const srcPath = path6.join(cwd, "src");
1332
+ const srcPath = path7.join(cwd, "src");
1209
1333
  if (!await pathExists(srcPath)) {
1210
1334
  log.error(
1211
1335
  "Run this command from your generated Next.js project root (missing ./src)."
@@ -1216,36 +1340,36 @@ function registerAddCommand(program2) {
1216
1340
  const featurePascal = toPascalCase(featureSlug);
1217
1341
  const modelPascal = singularizeWord(featurePascal);
1218
1342
  const modelDelegate = toCamelCase(modelPascal);
1219
- const featureRoot = path6.join(cwd, "src/features", featureSlug);
1343
+ const featureRoot = path7.join(cwd, "src/features", featureSlug);
1220
1344
  if (await pathExists(featureRoot)) {
1221
1345
  log.error(`Feature already exists: ${featureRoot}`);
1222
1346
  process.exitCode = 1;
1223
1347
  return;
1224
1348
  }
1225
- await ensureDir(path6.join(featureRoot, "api"));
1226
- await ensureDir(path6.join(featureRoot, "components"));
1227
- await writeFile5(
1228
- path6.join(featureRoot, "services.ts"),
1349
+ await ensureDir(path7.join(featureRoot, "api"));
1350
+ await ensureDir(path7.join(featureRoot, "components"));
1351
+ await writeFile6(
1352
+ path7.join(featureRoot, "services.ts"),
1229
1353
  buildFeatureServicesContent(modelPascal, modelDelegate),
1230
1354
  "utf8"
1231
1355
  );
1232
- await writeFile5(
1233
- path6.join(featureRoot, "validations.ts"),
1356
+ await writeFile6(
1357
+ path7.join(featureRoot, "validations.ts"),
1234
1358
  buildFeatureValidationContent(modelPascal),
1235
1359
  "utf8"
1236
1360
  );
1237
- await writeFile5(
1238
- path6.join(featureRoot, "api", `use-${featureSlug}.ts`),
1361
+ await writeFile6(
1362
+ path7.join(featureRoot, "api", `use-${featureSlug}.ts`),
1239
1363
  buildFeatureHooksContent(featureSlug, modelPascal),
1240
1364
  "utf8"
1241
1365
  );
1242
- await writeFile5(
1243
- path6.join(featureRoot, "components", `${featureSlug}-table.tsx`),
1366
+ await writeFile6(
1367
+ path7.join(featureRoot, "components", `${featureSlug}-table.tsx`),
1244
1368
  buildFeatureTableContent(featureSlug, modelPascal),
1245
1369
  "utf8"
1246
1370
  );
1247
- await writeFile5(
1248
- path6.join(
1371
+ await writeFile6(
1372
+ path7.join(
1249
1373
  featureRoot,
1250
1374
  "components",
1251
1375
  `create-${featureSlug}-dialog.tsx`
@@ -1253,39 +1377,39 @@ function registerAddCommand(program2) {
1253
1377
  buildFeatureDialogContent(featureSlug, modelPascal),
1254
1378
  "utf8"
1255
1379
  );
1256
- const routeFilePath = path6.join(
1380
+ const routeFilePath = path7.join(
1257
1381
  cwd,
1258
1382
  "src/app/api/v1",
1259
1383
  featureSlug,
1260
1384
  "route.ts"
1261
1385
  );
1262
- await ensureDir(path6.dirname(routeFilePath));
1263
- await writeFile5(
1386
+ await ensureDir(path7.dirname(routeFilePath));
1387
+ await writeFile6(
1264
1388
  routeFilePath,
1265
1389
  buildCollectionRouteContent(featureSlug, modelPascal),
1266
1390
  "utf8"
1267
1391
  );
1268
- const idRoutePath = path6.join(
1392
+ const idRoutePath = path7.join(
1269
1393
  cwd,
1270
1394
  "src/app/api/v1",
1271
1395
  featureSlug,
1272
1396
  "[id]",
1273
1397
  "route.ts"
1274
1398
  );
1275
- await ensureDir(path6.dirname(idRoutePath));
1276
- await writeFile5(
1399
+ await ensureDir(path7.dirname(idRoutePath));
1400
+ await writeFile6(
1277
1401
  idRoutePath,
1278
1402
  buildItemRouteContent(featureSlug, modelPascal),
1279
1403
  "utf8"
1280
1404
  );
1281
- const featurePagePath = path6.join(
1405
+ const featurePagePath = path7.join(
1282
1406
  cwd,
1283
1407
  "src/app/(dashboard)",
1284
1408
  featureSlug,
1285
1409
  "page.tsx"
1286
1410
  );
1287
- await ensureDir(path6.dirname(featurePagePath));
1288
- await writeFile5(
1411
+ await ensureDir(path7.dirname(featurePagePath));
1412
+ await writeFile6(
1289
1413
  featurePagePath,
1290
1414
  buildFeaturePageContent(featureSlug, modelPascal),
1291
1415
  "utf8"
@@ -1315,8 +1439,8 @@ function registerAddCommand(program2) {
1315
1439
  });
1316
1440
  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
1441
  const cwd = process.cwd();
1318
- const hasSrc = await pathExists(path6.join(cwd, "src"));
1319
- const hasPackageJson = await pathExists(path6.join(cwd, "package.json"));
1442
+ const hasSrc = await pathExists(path7.join(cwd, "src"));
1443
+ const hasPackageJson = await pathExists(path7.join(cwd, "package.json"));
1320
1444
  if (!hasSrc || !hasPackageJson) {
1321
1445
  log.error("Run this command from your generated Next.js project root.");
1322
1446
  process.exitCode = 1;
@@ -1376,7 +1500,7 @@ function registerAddCommand(program2) {
1376
1500
  if (Object.keys(envEntries).length > 0) {
1377
1501
  const envTargets = [".env", ".env.example", ".env.development"];
1378
1502
  for (const envFile of envTargets) {
1379
- const envPath = path6.join(cwd, envFile);
1503
+ const envPath = path7.join(cwd, envFile);
1380
1504
  if (await pathExists(envPath)) {
1381
1505
  await mergeEnvFile(envPath, envEntries);
1382
1506
  }
@@ -1386,7 +1510,7 @@ function registerAddCommand(program2) {
1386
1510
  const envTargets = [".env", ".env.example", ".env.development"];
1387
1511
  for (const envFile of envTargets) {
1388
1512
  await upsertEnvValue(
1389
- path6.join(cwd, envFile),
1513
+ path7.join(cwd, envFile),
1390
1514
  "NEXT_PUBLIC_ENABLE_CHAT",
1391
1515
  "true"
1392
1516
  );
@@ -1404,14 +1528,14 @@ function registerAddCommand(program2) {
1404
1528
  );
1405
1529
  if (Object.keys(dependencyEntries).length > 0) {
1406
1530
  await addDependencies(
1407
- path6.join(cwd, "package.json"),
1531
+ path7.join(cwd, "package.json"),
1408
1532
  dependencyEntries
1409
1533
  );
1410
1534
  }
1411
1535
  const state = await detectProjectState(cwd);
1412
1536
  const namespaceSet = new Set(state.namespaces);
1413
1537
  for (const moduleId of selectedModules) {
1414
- const moduleTemplateMessages = path6.join(
1538
+ const moduleTemplateMessages = path7.join(
1415
1539
  getModuleById(moduleId).templatePath,
1416
1540
  "messages/vi"
1417
1541
  );
@@ -1428,8 +1552,8 @@ function registerAddCommand(program2) {
1428
1552
  const namespace = file.name.replace(/\.json$/, "");
1429
1553
  namespaceSet.add(namespace);
1430
1554
  const templateData = JSON.parse(
1431
- await readFile5(
1432
- path6.join(moduleTemplateMessages, file.name),
1555
+ await readFile6(
1556
+ path7.join(moduleTemplateMessages, file.name),
1433
1557
  "utf8"
1434
1558
  )
1435
1559
  );
@@ -1442,6 +1566,10 @@ function registerAddCommand(program2) {
1442
1566
  namespaces: [...namespaceSet],
1443
1567
  modules: [.../* @__PURE__ */ new Set([...state.modules, ...selectedModules])]
1444
1568
  });
1569
+ const mergedModules = [
1570
+ .../* @__PURE__ */ new Set([...state.modules, ...selectedModules])
1571
+ ];
1572
+ await mergeModuleSetupSections(cwd, selectedModules, mergedModules);
1445
1573
  finishPrompt(`Added modules: ${selectedModules.join(", ")}`);
1446
1574
  log.detail("Copied files", String(copiedFileCount));
1447
1575
  if (autoAddedModules.length > 0) {
@@ -1472,8 +1600,8 @@ function registerAddCommand(program2) {
1472
1600
  });
1473
1601
  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
1602
  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"));
1603
+ const hasMessages = await pathExists(path7.join(cwd, "messages"));
1604
+ const hasConfig = await pathExists(path7.join(cwd, "src/i18n/config.ts"));
1477
1605
  if (!hasMessages || !hasConfig) {
1478
1606
  log.error(
1479
1607
  "Run this command from a generated Next.js project with i18n scaffold."
@@ -1540,9 +1668,9 @@ function registerAddCommand(program2) {
1540
1668
  });
1541
1669
  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
1670
  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"));
1671
+ const authFilePath = path7.join(cwd, "src/lib/auth.ts");
1672
+ const hasSrc = await pathExists(path7.join(cwd, "src"));
1673
+ const hasPackageJson = await pathExists(path7.join(cwd, "package.json"));
1546
1674
  const hasAuthFile = await pathExists(authFilePath);
1547
1675
  if (!hasSrc || !hasPackageJson || !hasAuthFile) {
1548
1676
  log.error(
@@ -1581,13 +1709,13 @@ function registerAddCommand(program2) {
1581
1709
  finishPrompt("No auth providers selected.");
1582
1710
  return;
1583
1711
  }
1584
- const authContent = await readFile5(authFilePath, "utf8");
1712
+ const authContent = await readFile6(authFilePath, "utf8");
1585
1713
  const existingProviders = readConfiguredProviders(authContent);
1586
1714
  const mergedProviders = [
1587
1715
  .../* @__PURE__ */ new Set([...existingProviders, ...selectedProviders])
1588
1716
  ];
1589
1717
  const nextAuthContent = patchAuthProviders(authContent, mergedProviders);
1590
- await writeFile5(authFilePath, nextAuthContent, "utf8");
1718
+ await writeFile6(authFilePath, nextAuthContent, "utf8");
1591
1719
  const envEntries = {};
1592
1720
  if (mergedProviders.includes("google")) {
1593
1721
  envEntries.GOOGLE_CLIENT_ID = "";
@@ -1599,7 +1727,7 @@ function registerAddCommand(program2) {
1599
1727
  }
1600
1728
  const envTargets = [".env", ".env.example", ".env.development"];
1601
1729
  for (const envFile of envTargets) {
1602
- const envPath = path6.join(cwd, envFile);
1730
+ const envPath = path7.join(cwd, envFile);
1603
1731
  if (await pathExists(envPath)) {
1604
1732
  await mergeEnvFile(envPath, envEntries);
1605
1733
  }
@@ -1615,7 +1743,7 @@ function registerAddCommand(program2) {
1615
1743
  // src/commands/create.ts
1616
1744
  import { spawn as spawn2 } from "child_process";
1617
1745
  import { randomBytes } from "crypto";
1618
- import path7 from "path";
1746
+ import path8 from "path";
1619
1747
  async function runInstall(packageManager, cwd) {
1620
1748
  const installArgsMap = {
1621
1749
  npm: ["install"],
@@ -1671,7 +1799,7 @@ async function resolveProjectName() {
1671
1799
  }
1672
1800
  }
1673
1801
  });
1674
- const targetPath = path7.resolve(process.cwd(), projectName);
1802
+ const targetPath = path8.resolve(process.cwd(), projectName);
1675
1803
  if (await pathExists(targetPath)) {
1676
1804
  log.error(`Target directory already exists: ${targetPath}`);
1677
1805
  continue;
@@ -1683,8 +1811,8 @@ function registerCreateCommand(program2) {
1683
1811
  program2.command("create").description("Create a new outsource-ready Next.js app").action(async () => {
1684
1812
  startPrompt("NexTCLI project creation");
1685
1813
  const projectName = await resolveProjectName();
1686
- const targetPath = path7.resolve(process.cwd(), projectName);
1687
- const projectDirectoryName = path7.basename(targetPath);
1814
+ const targetPath = path8.resolve(process.cwd(), projectName);
1815
+ const projectDirectoryName = path8.basename(targetPath);
1688
1816
  const projectSlug = toProjectSlug(projectDirectoryName);
1689
1817
  const packageManager = await askSelect(
1690
1818
  "Which package manager do you want to use?",
@@ -1733,7 +1861,7 @@ function registerCreateCommand(program2) {
1733
1861
  if (Object.keys(moduleEnvEntries).length > 0) {
1734
1862
  const envTargets = [".env", ".env.example", ".env.development"];
1735
1863
  for (const envFile of envTargets) {
1736
- const envPath = path7.join(targetPath, envFile);
1864
+ const envPath = path8.join(targetPath, envFile);
1737
1865
  if (await pathExists(envPath)) {
1738
1866
  await mergeEnvFile(envPath, moduleEnvEntries);
1739
1867
  }
@@ -1751,7 +1879,7 @@ function registerCreateCommand(program2) {
1751
1879
  );
1752
1880
  if (Object.keys(dependencyEntries).length > 0) {
1753
1881
  await addDependencies(
1754
- path7.join(targetPath, "package.json"),
1882
+ path8.join(targetPath, "package.json"),
1755
1883
  dependencyEntries
1756
1884
  );
1757
1885
  }
@@ -1760,13 +1888,18 @@ function registerCreateCommand(program2) {
1760
1888
  __PROJECT_NAME__: projectSlug,
1761
1889
  __ENABLE_CHAT__: selectedModules.includes("chat") ? "true" : "false",
1762
1890
  __BETTER_AUTH_SECRET__: betterAuthSecret,
1763
- __NEXTCLI_VERSION__: "0.3.0"
1891
+ __NEXTCLI_VERSION__: "0.4.0"
1764
1892
  });
1893
+ await mergeModuleSetupSections(
1894
+ targetPath,
1895
+ selectedModules,
1896
+ selectedModules
1897
+ );
1765
1898
  const manifest = await readManifest(targetPath);
1766
1899
  if (manifest) {
1767
1900
  await writeManifest(targetPath, {
1768
1901
  ...manifest,
1769
- cli: "0.3.0",
1902
+ cli: "0.4.0",
1770
1903
  modules: selectedModules
1771
1904
  });
1772
1905
  }
@@ -1791,13 +1924,15 @@ function registerCreateCommand(program2) {
1791
1924
  "Optional chat schema block was appended to prisma/schema.prisma."
1792
1925
  );
1793
1926
  }
1794
- log.step(`Next: cd ${projectName} && ${packageManager} run dev`);
1927
+ log.step(
1928
+ `Next: cd ${projectName} && ${packageManager} run db:migrate && ${packageManager} run dev`
1929
+ );
1795
1930
  });
1796
1931
  }
1797
1932
 
1798
1933
  // src/commands/migrate.ts
1799
1934
  import { spawn as spawn3 } from "child_process";
1800
- import path8 from "path";
1935
+ import path9 from "path";
1801
1936
  function createDefaultMigrationName() {
1802
1937
  const now = /* @__PURE__ */ new Date();
1803
1938
  const y = now.getFullYear();
@@ -1809,16 +1944,16 @@ function createDefaultMigrationName() {
1809
1944
  return `auto_${y}${m}${d}${hh}${mm}${ss}`;
1810
1945
  }
1811
1946
  async function detectPackageManager(cwd) {
1812
- if (await pathExists(path8.join(cwd, "bun.lockb"))) {
1947
+ if (await pathExists(path9.join(cwd, "bun.lockb"))) {
1813
1948
  return "bun";
1814
1949
  }
1815
- if (await pathExists(path8.join(cwd, "bun.lock"))) {
1950
+ if (await pathExists(path9.join(cwd, "bun.lock"))) {
1816
1951
  return "bun";
1817
1952
  }
1818
- if (await pathExists(path8.join(cwd, "pnpm-lock.yaml"))) {
1953
+ if (await pathExists(path9.join(cwd, "pnpm-lock.yaml"))) {
1819
1954
  return "pnpm";
1820
1955
  }
1821
- if (await pathExists(path8.join(cwd, "yarn.lock"))) {
1956
+ if (await pathExists(path9.join(cwd, "yarn.lock"))) {
1822
1957
  return "yarn";
1823
1958
  }
1824
1959
  return "npm";
@@ -1853,8 +1988,8 @@ async function runCommand2(command, args, cwd) {
1853
1988
  function registerMigrateCommand(program2) {
1854
1989
  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
1990
  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"));
1991
+ const hasPackageJson = await pathExists(path9.join(cwd, "package.json"));
1992
+ const hasPrismaSchema = await pathExists(path9.join(cwd, "prisma", "schema.prisma"));
1858
1993
  if (!hasPackageJson || !hasPrismaSchema) {
1859
1994
  log.error(
1860
1995
  "Run this command from a generated project root (requires package.json + prisma/schema.prisma)."
@@ -2006,7 +2141,7 @@ var NexTCLICommand = class _NexTCLICommand extends Command {
2006
2141
 
2007
2142
  // src/cli.ts
2008
2143
  var program = new NexTCLICommand();
2009
- program.name("nextcli").description("Scaffold outsource-ready Next.js projects").version("0.3.0");
2144
+ program.name("nextcli").description("Scaffold outsource-ready Next.js projects").version("0.4.0");
2010
2145
  registerCreateCommand(program);
2011
2146
  registerAddCommand(program);
2012
2147
  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.0",
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.0",
36
37
  "commander": "^12.1.0"
37
38
  },
38
39
  "devDependencies": {