@thinhnguyencth1204/nextcli 0.2.1 → 0.3.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 (68) hide show
  1. package/dist/cli.js +632 -90
  2. package/package.json +1 -1
  3. package/templates/next-base/components.json +21 -0
  4. package/templates/next-base/messages/vi/auth.json +28 -0
  5. package/templates/next-base/messages/vi/common.json +34 -0
  6. package/templates/next-base/messages/vi/example.json +10 -0
  7. package/templates/next-base/next.config.ts +11 -1
  8. package/templates/next-base/nextcli.json +8 -0
  9. package/templates/next-base/package.json +21 -1
  10. package/templates/next-base/postcss.config.mjs +5 -0
  11. package/templates/next-base/src/app/(auth)/layout.tsx +9 -0
  12. package/templates/next-base/src/app/(auth)/sign-in/page.tsx +6 -3
  13. package/templates/next-base/src/app/(dashboard)/account/page.tsx +9 -5
  14. package/templates/next-base/src/app/(dashboard)/dashboard/page.tsx +17 -0
  15. package/templates/next-base/src/app/(dashboard)/example/page.tsx +5 -2
  16. package/templates/next-base/src/app/(dashboard)/layout.tsx +10 -0
  17. package/templates/next-base/src/app/globals.css +107 -0
  18. package/templates/next-base/src/app/layout.tsx +18 -8
  19. package/templates/next-base/src/app/page.tsx +2 -18
  20. package/templates/next-base/src/components/layout/private/app-sidebar.tsx +45 -0
  21. package/templates/next-base/src/components/layout/private/dashboard-layout.tsx +53 -0
  22. package/templates/next-base/src/components/layout/private/locale-switcher.tsx +45 -0
  23. package/templates/next-base/src/components/layout/private/nav-sidebar.tsx +55 -0
  24. package/templates/next-base/src/components/layout/private/nav-user.tsx +99 -0
  25. package/templates/next-base/src/components/providers/theme-provider.tsx +11 -0
  26. package/templates/next-base/src/components/ui/alert-dialog.tsx +11 -0
  27. package/templates/next-base/src/components/ui/avatar.tsx +45 -0
  28. package/templates/next-base/src/components/ui/badge.tsx +29 -0
  29. package/templates/next-base/src/components/ui/button.tsx +47 -7
  30. package/templates/next-base/src/components/ui/card.tsx +54 -0
  31. package/templates/next-base/src/components/ui/data-table/data-table-column-header.tsx +23 -0
  32. package/templates/next-base/src/components/ui/data-table/data-table-filter-list.tsx +3 -0
  33. package/templates/next-base/src/components/ui/data-table/data-table-pagination.tsx +35 -0
  34. package/templates/next-base/src/components/ui/data-table/data-table-skeleton.tsx +11 -0
  35. package/templates/next-base/src/components/ui/data-table/data-table-toolbar.tsx +14 -0
  36. package/templates/next-base/src/components/ui/data-table/data-table-view-options.tsx +3 -0
  37. package/templates/next-base/src/components/ui/data-table/data-table.tsx +72 -0
  38. package/templates/next-base/src/components/ui/dialog.tsx +105 -0
  39. package/templates/next-base/src/components/ui/dropdown-menu.tsx +44 -0
  40. package/templates/next-base/src/components/ui/input.tsx +19 -0
  41. package/templates/next-base/src/components/ui/label.tsx +15 -0
  42. package/templates/next-base/src/components/ui/popover.tsx +30 -0
  43. package/templates/next-base/src/components/ui/scroll-area.tsx +47 -0
  44. package/templates/next-base/src/components/ui/select.tsx +76 -0
  45. package/templates/next-base/src/components/ui/separator.tsx +23 -0
  46. package/templates/next-base/src/components/ui/sheet.tsx +117 -0
  47. package/templates/next-base/src/components/ui/sidebar.tsx +215 -0
  48. package/templates/next-base/src/components/ui/skeleton.tsx +10 -0
  49. package/templates/next-base/src/components/ui/sonner.tsx +3 -0
  50. package/templates/next-base/src/components/ui/table.tsx +54 -0
  51. package/templates/next-base/src/components/ui/tabs.tsx +52 -0
  52. package/templates/next-base/src/components/ui/textarea.tsx +17 -0
  53. package/templates/next-base/src/components/ui/tooltip.tsx +26 -0
  54. package/templates/next-base/src/data/sidebar-modules.ts +11 -0
  55. package/templates/next-base/src/example/components/example-table.tsx +25 -40
  56. package/templates/next-base/src/features/auth/components/account-panel.tsx +21 -8
  57. package/templates/next-base/src/features/auth/components/sign-in-form.tsx +43 -30
  58. package/templates/next-base/src/hooks/index.ts +1 -1
  59. package/templates/next-base/src/hooks/table/use-data-table.ts +33 -0
  60. package/templates/next-base/src/hooks/use-mobile.ts +25 -0
  61. package/templates/next-base/src/i18n/config.ts +7 -0
  62. package/templates/next-base/src/i18n/namespaces.ts +5 -0
  63. package/templates/next-base/src/i18n/request.ts +19 -2
  64. package/templates/next-base/src/lib/prisma.ts +11 -1
  65. package/templates/next-base/src/types/data-table.ts +4 -0
  66. package/templates/next-base/src/types/index.ts +2 -0
  67. package/templates/next-base/middleware.ts +0 -10
  68. package/templates/next-base/src/app/styles.css +0 -12
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 path4 from "path";
4
+ import path6 from "path";
5
5
 
6
6
  // src/core/fs.ts
7
7
  import {
@@ -157,7 +157,7 @@ async function addDependencies(packageJsonPath, dependencies) {
157
157
  }
158
158
 
159
159
  // src/commands/add.ts
160
- import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
160
+ import { readdir as readdir4, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
161
161
 
162
162
  // src/core/templates.ts
163
163
  import path2 from "path";
@@ -501,6 +501,184 @@ async function ensureChatSchemaInProject(projectRoot) {
501
501
  return "added";
502
502
  }
503
503
 
504
+ // src/core/i18n.ts
505
+ import path5 from "path";
506
+ import { readdir as readdir3, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
507
+
508
+ // src/core/manifest.ts
509
+ import path4 from "path";
510
+ import { readdir as readdir2, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
511
+ var defaultManifest = {
512
+ cli: "0.3.0",
513
+ defaultLocale: "vi",
514
+ locales: ["vi"],
515
+ namespaces: ["common", "auth", "example"],
516
+ modules: [],
517
+ features: ["example"]
518
+ };
519
+ function getManifestPath(projectDir) {
520
+ return path4.join(projectDir, "nextcli.json");
521
+ }
522
+ async function readManifest(projectDir) {
523
+ const manifestPath = getManifestPath(projectDir);
524
+ if (!await pathExists(manifestPath)) {
525
+ return null;
526
+ }
527
+ const raw = await readFile3(manifestPath, "utf8");
528
+ return JSON.parse(raw);
529
+ }
530
+ async function writeManifest(projectDir, manifest) {
531
+ const manifestPath = getManifestPath(projectDir);
532
+ await writeFile3(
533
+ manifestPath,
534
+ `${JSON.stringify(manifest, null, 2)}
535
+ `,
536
+ "utf8"
537
+ );
538
+ }
539
+ function parseConstArray(content, marker) {
540
+ const regex = marker === "locales" ? /nextcli:locales:start[\s\S]*?=\s*\[(.*?)\]\s*as const[\s\S]*?nextcli:locales:end/m : /nextcli:namespaces:start[\s\S]*?=\s*\[(.*?)\]\s*as const[\s\S]*?nextcli:namespaces:end/m;
541
+ const match = content.match(regex);
542
+ if (!match) {
543
+ return [];
544
+ }
545
+ return match[1].split(",").map((item) => item.trim().replaceAll('"', "").replaceAll("'", "")).filter(Boolean);
546
+ }
547
+ async function detectLocalesFromDisk(projectDir) {
548
+ const messagesDir = path4.join(projectDir, "messages");
549
+ if (!await pathExists(messagesDir)) {
550
+ return ["vi"];
551
+ }
552
+ const entries = await readdir2(messagesDir, { withFileTypes: true });
553
+ const locales = entries.filter((item) => item.isDirectory()).map((item) => item.name);
554
+ return locales.length > 0 ? locales.sort() : ["vi"];
555
+ }
556
+ async function detectNamespacesFromDisk(projectDir) {
557
+ const namespaceFile = path4.join(projectDir, "src/i18n/namespaces.ts");
558
+ if (!await pathExists(namespaceFile)) {
559
+ return [...defaultManifest.namespaces];
560
+ }
561
+ const content = await readFile3(namespaceFile, "utf8");
562
+ const namespaces = parseConstArray(content, "namespaces");
563
+ return namespaces.length > 0 ? namespaces : [...defaultManifest.namespaces];
564
+ }
565
+ async function reconcileManifest(projectDir) {
566
+ const localesFromDisk = await detectLocalesFromDisk(projectDir);
567
+ const namespacesFromDisk = await detectNamespacesFromDisk(projectDir);
568
+ const existing = await readManifest(projectDir);
569
+ const merged = {
570
+ ...existing ?? defaultManifest,
571
+ locales: [.../* @__PURE__ */ new Set([...existing?.locales ?? [], ...localesFromDisk])],
572
+ namespaces: [
573
+ .../* @__PURE__ */ new Set([...existing?.namespaces ?? [], ...namespacesFromDisk])
574
+ ]
575
+ };
576
+ merged.defaultLocale = merged.locales.includes(merged.defaultLocale) ? merged.defaultLocale : merged.locales[0] ?? "vi";
577
+ merged.modules = [...new Set(merged.modules)];
578
+ merged.features = [...new Set(merged.features)];
579
+ await writeManifest(projectDir, merged);
580
+ return merged;
581
+ }
582
+
583
+ // src/core/i18n.ts
584
+ var localeStartMarker = "// nextcli:locales:start";
585
+ var localeEndMarker = "// nextcli:locales:end";
586
+ var namespaceStartMarker = "// nextcli:namespaces:start";
587
+ var namespaceEndMarker = "// nextcli:namespaces:end";
588
+ function formatArray(items) {
589
+ return items.map((item) => `"${item}"`).join(", ");
590
+ }
591
+ function patchBetweenMarkers(content, start, end, replacement) {
592
+ const regex = new RegExp(`(${start})([\\s\\S]*?)(${end})`, "m");
593
+ return content.replace(regex, `$1
594
+ ${replacement}
595
+ $3`);
596
+ }
597
+ function deepCloneValue(value) {
598
+ return JSON.parse(JSON.stringify(value));
599
+ }
600
+ async function detectProjectState(projectDir) {
601
+ return reconcileManifest(projectDir);
602
+ }
603
+ async function patchLocalesConfig(projectDir, locales) {
604
+ const configPath = path5.join(projectDir, "src/i18n/config.ts");
605
+ if (!await pathExists(configPath)) {
606
+ return;
607
+ }
608
+ const content = await readFile4(configPath, "utf8");
609
+ const next = patchBetweenMarkers(
610
+ content,
611
+ localeStartMarker,
612
+ localeEndMarker,
613
+ `export const locales = [${formatArray(locales)}] as const;`
614
+ );
615
+ await writeFile4(configPath, next, "utf8");
616
+ }
617
+ async function appendNamespace(projectDir, namespace) {
618
+ const namespacePath = path5.join(projectDir, "src/i18n/namespaces.ts");
619
+ if (!await pathExists(namespacePath)) {
620
+ return [];
621
+ }
622
+ const currentState = await detectProjectState(projectDir);
623
+ const namespaces = [.../* @__PURE__ */ new Set([...currentState.namespaces, namespace])];
624
+ const content = await readFile4(namespacePath, "utf8");
625
+ const next = patchBetweenMarkers(
626
+ content,
627
+ namespaceStartMarker,
628
+ namespaceEndMarker,
629
+ `export const namespaces = [${formatArray(namespaces)}] as const;`
630
+ );
631
+ await writeFile4(namespacePath, next, "utf8");
632
+ await writeManifest(projectDir, {
633
+ ...currentState,
634
+ namespaces
635
+ });
636
+ return namespaces;
637
+ }
638
+ async function cloneLocaleMessages(projectDir, fromLocale, toLocale) {
639
+ const sourceDir = path5.join(projectDir, "messages", fromLocale);
640
+ const targetDir = path5.join(projectDir, "messages", toLocale);
641
+ if (!await pathExists(sourceDir)) {
642
+ return;
643
+ }
644
+ const entries = await readdir3(sourceDir, { withFileTypes: true });
645
+ if (!await pathExists(targetDir)) {
646
+ await ensureDir(targetDir);
647
+ }
648
+ for (const entry of entries) {
649
+ if (!entry.isFile() || !entry.name.endsWith(".json")) {
650
+ continue;
651
+ }
652
+ const sourcePath = path5.join(sourceDir, entry.name);
653
+ const targetPath = path5.join(targetDir, entry.name);
654
+ const sourceContent = JSON.parse(
655
+ await readFile4(sourcePath, "utf8")
656
+ );
657
+ await writeFile4(
658
+ targetPath,
659
+ `${JSON.stringify(deepCloneValue(sourceContent), null, 2)}
660
+ `,
661
+ "utf8"
662
+ );
663
+ }
664
+ }
665
+ async function writeNamespaceMessages(projectDir, namespace, viTemplate) {
666
+ const state = await detectProjectState(projectDir);
667
+ for (const locale of state.locales) {
668
+ const localeDir = path5.join(projectDir, "messages", locale);
669
+ if (!await pathExists(localeDir)) {
670
+ continue;
671
+ }
672
+ const filePath = path5.join(localeDir, `${namespace}.json`);
673
+ await writeFile4(
674
+ filePath,
675
+ `${JSON.stringify(deepCloneValue(viTemplate), null, 2)}
676
+ `,
677
+ "utf8"
678
+ );
679
+ }
680
+ }
681
+
504
682
  // src/commands/add.ts
505
683
  function toKebabCase(input) {
506
684
  return input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
@@ -654,6 +832,172 @@ export function useDelete${modelPascal}() {
654
832
  }
655
833
  `;
656
834
  }
835
+ function buildFeatureTableContent(featureSlug, modelPascal) {
836
+ return `"use client";
837
+
838
+ import { useMemo } from "react";
839
+ import { useTranslations } from "next-intl";
840
+ import { createColumnHelper, getCoreRowModel, getPaginationRowModel, useReactTable } from "@tanstack/react-table";
841
+ import { DataTable } from "@/components/ui/data-table/data-table";
842
+ import { use${modelPascal}s } from "@/features/${featureSlug}/api/use-${featureSlug}";
843
+
844
+ type ${modelPascal}Item = {
845
+ id: string;
846
+ name: string;
847
+ description?: string | null;
848
+ createdAt: string;
849
+ updatedAt: string;
850
+ };
851
+
852
+ const columnHelper = createColumnHelper<${modelPascal}Item>();
853
+
854
+ export function ${modelPascal}Table() {
855
+ const t = useTranslations("${featureSlug}");
856
+ const { data, isLoading } = use${modelPascal}s();
857
+
858
+ const columns = useMemo(
859
+ () => [
860
+ columnHelper.accessor("name", {
861
+ header: t("table.name"),
862
+ }),
863
+ columnHelper.accessor("description", {
864
+ header: t("table.description"),
865
+ }),
866
+ ],
867
+ [t],
868
+ );
869
+
870
+ const table = useReactTable({
871
+ data: Array.isArray(data) ? data : [],
872
+ columns,
873
+ getCoreRowModel: getCoreRowModel(),
874
+ getPaginationRowModel: getPaginationRowModel(),
875
+ });
876
+
877
+ if (isLoading) {
878
+ return <p>{t("table.loading")}</p>;
879
+ }
880
+
881
+ return <DataTable table={table} />;
882
+ }
883
+ `;
884
+ }
885
+ function buildFeatureDialogContent(featureSlug, modelPascal) {
886
+ return `"use client";
887
+
888
+ import { useState, type FormEvent } from "react";
889
+ import { useTranslations } from "next-intl";
890
+ import { Button } from "@/components/ui/button";
891
+ import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
892
+ import { Input } from "@/components/ui/input";
893
+ import { Label } from "@/components/ui/label";
894
+
895
+ export function Create${modelPascal}Dialog({
896
+ onCreate,
897
+ }: {
898
+ onCreate: (payload: { name: string; description?: string }) => Promise<void>;
899
+ }) {
900
+ const t = useTranslations("${featureSlug}");
901
+ const [open, setOpen] = useState(false);
902
+ const [name, setName] = useState("");
903
+ const [description, setDescription] = useState("");
904
+ const [submitting, setSubmitting] = useState(false);
905
+
906
+ const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
907
+ event.preventDefault();
908
+ setSubmitting(true);
909
+ try {
910
+ await onCreate({ name, description: description || undefined });
911
+ setName("");
912
+ setDescription("");
913
+ setOpen(false);
914
+ } finally {
915
+ setSubmitting(false);
916
+ }
917
+ };
918
+
919
+ return (
920
+ <Dialog open={open} onOpenChange={setOpen}>
921
+ <DialogTrigger asChild>
922
+ <Button>{t("dialog.open")}</Button>
923
+ </DialogTrigger>
924
+ <DialogContent>
925
+ <DialogHeader>
926
+ <DialogTitle>{t("dialog.title")}</DialogTitle>
927
+ </DialogHeader>
928
+ <form className="grid gap-4" onSubmit={handleSubmit}>
929
+ <div className="grid gap-2">
930
+ <Label htmlFor="name">{t("dialog.name")}</Label>
931
+ <Input id="name" value={name} onChange={(event) => setName(event.target.value)} required />
932
+ </div>
933
+ <div className="grid gap-2">
934
+ <Label htmlFor="description">{t("dialog.description")}</Label>
935
+ <Input
936
+ id="description"
937
+ value={description}
938
+ onChange={(event) => setDescription(event.target.value)}
939
+ />
940
+ </div>
941
+ <DialogFooter>
942
+ <Button type="submit" disabled={submitting}>
943
+ {submitting ? t("dialog.submitting") : t("dialog.submit")}
944
+ </Button>
945
+ </DialogFooter>
946
+ </form>
947
+ </DialogContent>
948
+ </Dialog>
949
+ );
950
+ }
951
+ `;
952
+ }
953
+ function buildFeaturePageContent(featureSlug, modelPascal) {
954
+ return `"use client";
955
+
956
+ import { useTranslations } from "next-intl";
957
+ import { useCreate${modelPascal} } from "@/features/${featureSlug}/api/use-${featureSlug}";
958
+ import { Create${modelPascal}Dialog } from "@/features/${featureSlug}/components/create-${featureSlug}-dialog";
959
+ import { ${modelPascal}Table } from "@/features/${featureSlug}/components/${featureSlug}-table";
960
+
961
+ export default function ${modelPascal}Page() {
962
+ const t = useTranslations("${featureSlug}");
963
+ const createMutation = useCreate${modelPascal}();
964
+
965
+ return (
966
+ <main className="space-y-4">
967
+ <div className="flex items-center justify-between">
968
+ <h1 className="text-2xl font-semibold">{t("page.title")}</h1>
969
+ <Create${modelPascal}Dialog
970
+ onCreate={async (payload) => {
971
+ await createMutation.mutateAsync(payload);
972
+ }}
973
+ />
974
+ </div>
975
+ <${modelPascal}Table />
976
+ </main>
977
+ );
978
+ }
979
+ `;
980
+ }
981
+ function buildFeatureMessages(featureName) {
982
+ return {
983
+ page: {
984
+ title: featureName
985
+ },
986
+ table: {
987
+ name: "T\xEAn",
988
+ description: "M\xF4 t\u1EA3",
989
+ loading: "\u0110ang t\u1EA3i d\u1EEF li\u1EC7u..."
990
+ },
991
+ dialog: {
992
+ open: "T\u1EA1o m\u1EDBi",
993
+ title: `T\u1EA1o ${featureName}`,
994
+ name: "T\xEAn",
995
+ description: "M\xF4 t\u1EA3",
996
+ submit: "L\u01B0u",
997
+ submitting: "\u0110ang l\u01B0u..."
998
+ }
999
+ };
1000
+ }
657
1001
  function buildCollectionRouteContent(featureSlug, modelPascal) {
658
1002
  return `import { fail, ok } from "@/lib/api-response";
659
1003
  import {
@@ -750,11 +1094,11 @@ export async function DELETE(
750
1094
  `;
751
1095
  }
752
1096
  async function appendFeatureModelToPrismaSchema(cwd, modelPascal) {
753
- const schemaPath = path4.join(cwd, "prisma", "schema.prisma");
1097
+ const schemaPath = path6.join(cwd, "prisma", "schema.prisma");
754
1098
  if (!await pathExists(schemaPath)) {
755
1099
  return "skipped";
756
1100
  }
757
- const schemaContent = await readFile3(schemaPath, "utf8");
1101
+ const schemaContent = await readFile5(schemaPath, "utf8");
758
1102
  const modelRegex = new RegExp(`\\bmodel\\s+${modelPascal}\\b`);
759
1103
  if (modelRegex.test(schemaContent)) {
760
1104
  return "exists";
@@ -771,8 +1115,12 @@ model ${modelPascal} {
771
1115
  updatedAt DateTime @updatedAt
772
1116
  }
773
1117
  `;
774
- await writeFile3(schemaPath, `${schemaContent.trimEnd()}${modelBlock}
775
- `, "utf8");
1118
+ await writeFile5(
1119
+ schemaPath,
1120
+ `${schemaContent.trimEnd()}${modelBlock}
1121
+ `,
1122
+ "utf8"
1123
+ );
776
1124
  return "added";
777
1125
  }
778
1126
  var authProviderStartMarker = "// AUTO_GENERATED_AUTH_PROVIDERS_START";
@@ -833,18 +1181,18 @@ async function upsertEnvValue(envFilePath, key, value) {
833
1181
  if (!await pathExists(envFilePath)) {
834
1182
  return;
835
1183
  }
836
- const content = await readFile3(envFilePath, "utf8");
1184
+ const content = await readFile5(envFilePath, "utf8");
837
1185
  const entry = `${key}=${value}`;
838
1186
  const pattern = new RegExp(`^${key}=.*$`, "m");
839
1187
  if (pattern.test(content)) {
840
1188
  const next = content.replace(pattern, entry);
841
1189
  if (next !== content) {
842
- await writeFile3(envFilePath, next, "utf8");
1190
+ await writeFile5(envFilePath, next, "utf8");
843
1191
  }
844
1192
  return;
845
1193
  }
846
1194
  const separator = content.endsWith("\n") || content.length === 0 ? "" : "\n";
847
- await writeFile3(envFilePath, `${content}${separator}${entry}
1195
+ await writeFile5(envFilePath, `${content}${separator}${entry}
848
1196
  `, "utf8");
849
1197
  }
850
1198
  function registerAddCommand(program2) {
@@ -857,54 +1205,118 @@ function registerAddCommand(program2) {
857
1205
  return;
858
1206
  }
859
1207
  const cwd = process.cwd();
860
- const srcPath = path4.join(cwd, "src");
1208
+ const srcPath = path6.join(cwd, "src");
861
1209
  if (!await pathExists(srcPath)) {
862
- log.error("Run this command from your generated Next.js project root (missing ./src).");
1210
+ log.error(
1211
+ "Run this command from your generated Next.js project root (missing ./src)."
1212
+ );
863
1213
  process.exitCode = 1;
864
1214
  return;
865
1215
  }
866
1216
  const featurePascal = toPascalCase(featureSlug);
867
1217
  const modelPascal = singularizeWord(featurePascal);
868
1218
  const modelDelegate = toCamelCase(modelPascal);
869
- const featureRoot = path4.join(cwd, "src/features", featureSlug);
1219
+ const featureRoot = path6.join(cwd, "src/features", featureSlug);
870
1220
  if (await pathExists(featureRoot)) {
871
1221
  log.error(`Feature already exists: ${featureRoot}`);
872
1222
  process.exitCode = 1;
873
1223
  return;
874
1224
  }
875
- await ensureDir(path4.join(featureRoot, "api"));
876
- await ensureDir(path4.join(featureRoot, "components"));
877
- await writeFile3(
878
- path4.join(featureRoot, "services.ts"),
1225
+ await ensureDir(path6.join(featureRoot, "api"));
1226
+ await ensureDir(path6.join(featureRoot, "components"));
1227
+ await writeFile5(
1228
+ path6.join(featureRoot, "services.ts"),
879
1229
  buildFeatureServicesContent(modelPascal, modelDelegate),
880
1230
  "utf8"
881
1231
  );
882
- await writeFile3(
883
- path4.join(featureRoot, "validations.ts"),
1232
+ await writeFile5(
1233
+ path6.join(featureRoot, "validations.ts"),
884
1234
  buildFeatureValidationContent(modelPascal),
885
1235
  "utf8"
886
1236
  );
887
- await writeFile3(
888
- path4.join(featureRoot, "api", `use-${featureSlug}.ts`),
1237
+ await writeFile5(
1238
+ path6.join(featureRoot, "api", `use-${featureSlug}.ts`),
889
1239
  buildFeatureHooksContent(featureSlug, modelPascal),
890
1240
  "utf8"
891
1241
  );
892
- const routeFilePath = path4.join(cwd, "src/app/api/v1", featureSlug, "route.ts");
893
- await ensureDir(path4.dirname(routeFilePath));
894
- await writeFile3(routeFilePath, buildCollectionRouteContent(featureSlug, modelPascal), "utf8");
895
- const idRoutePath = path4.join(cwd, "src/app/api/v1", featureSlug, "[id]", "route.ts");
896
- await ensureDir(path4.dirname(idRoutePath));
897
- await writeFile3(idRoutePath, buildItemRouteContent(featureSlug, modelPascal), "utf8");
898
- const schemaStatus = await appendFeatureModelToPrismaSchema(cwd, modelPascal);
1242
+ await writeFile5(
1243
+ path6.join(featureRoot, "components", `${featureSlug}-table.tsx`),
1244
+ buildFeatureTableContent(featureSlug, modelPascal),
1245
+ "utf8"
1246
+ );
1247
+ await writeFile5(
1248
+ path6.join(
1249
+ featureRoot,
1250
+ "components",
1251
+ `create-${featureSlug}-dialog.tsx`
1252
+ ),
1253
+ buildFeatureDialogContent(featureSlug, modelPascal),
1254
+ "utf8"
1255
+ );
1256
+ const routeFilePath = path6.join(
1257
+ cwd,
1258
+ "src/app/api/v1",
1259
+ featureSlug,
1260
+ "route.ts"
1261
+ );
1262
+ await ensureDir(path6.dirname(routeFilePath));
1263
+ await writeFile5(
1264
+ routeFilePath,
1265
+ buildCollectionRouteContent(featureSlug, modelPascal),
1266
+ "utf8"
1267
+ );
1268
+ const idRoutePath = path6.join(
1269
+ cwd,
1270
+ "src/app/api/v1",
1271
+ featureSlug,
1272
+ "[id]",
1273
+ "route.ts"
1274
+ );
1275
+ await ensureDir(path6.dirname(idRoutePath));
1276
+ await writeFile5(
1277
+ idRoutePath,
1278
+ buildItemRouteContent(featureSlug, modelPascal),
1279
+ "utf8"
1280
+ );
1281
+ const featurePagePath = path6.join(
1282
+ cwd,
1283
+ "src/app/(dashboard)",
1284
+ featureSlug,
1285
+ "page.tsx"
1286
+ );
1287
+ await ensureDir(path6.dirname(featurePagePath));
1288
+ await writeFile5(
1289
+ featurePagePath,
1290
+ buildFeaturePageContent(featureSlug, modelPascal),
1291
+ "utf8"
1292
+ );
1293
+ const manifestState = await detectProjectState(cwd);
1294
+ await writeNamespaceMessages(
1295
+ cwd,
1296
+ featureSlug,
1297
+ buildFeatureMessages(modelPascal)
1298
+ );
1299
+ const namespaces = await appendNamespace(cwd, featureSlug);
1300
+ await writeManifest(cwd, {
1301
+ ...manifestState,
1302
+ namespaces,
1303
+ features: [.../* @__PURE__ */ new Set([...manifestState.features, featureSlug])]
1304
+ });
1305
+ const schemaStatus = await appendFeatureModelToPrismaSchema(
1306
+ cwd,
1307
+ modelPascal
1308
+ );
899
1309
  const schemaMessage = schemaStatus === "added" ? `Model ${modelPascal} appended to prisma/schema.prisma` : schemaStatus === "exists" ? `Model ${modelPascal} already exists in prisma/schema.prisma` : "Skipped prisma/schema.prisma update (file not found)";
900
1310
  log.success(`Feature generated with CRUD: src/features/${featureSlug}`);
901
1311
  log.info(schemaMessage);
902
- log.warn("No migration was executed. Run your migration command manually when ready.");
1312
+ log.warn(
1313
+ "No migration was executed. Run your migration command manually when ready."
1314
+ );
903
1315
  });
904
1316
  add.command("module").description("Add optional modules using interactive multi-select").option("--module <module...>", "Preselect module ids").option("--yes", "Skip prompts").action(async (options) => {
905
1317
  const cwd = process.cwd();
906
- const hasSrc = await pathExists(path4.join(cwd, "src"));
907
- const hasPackageJson = await pathExists(path4.join(cwd, "package.json"));
1318
+ const hasSrc = await pathExists(path6.join(cwd, "src"));
1319
+ const hasPackageJson = await pathExists(path6.join(cwd, "package.json"));
908
1320
  if (!hasSrc || !hasPackageJson) {
909
1321
  log.error("Run this command from your generated Next.js project root.");
910
1322
  process.exitCode = 1;
@@ -951,46 +1363,85 @@ function registerAddCommand(program2) {
951
1363
  if (selectedModules.includes("chat")) {
952
1364
  chatSchemaStatus = await ensureChatSchemaInProject(cwd);
953
1365
  }
954
- const envEntries = selectedModules.reduce((acc, moduleId) => {
955
- const module = getModuleById(moduleId);
956
- return {
957
- ...acc,
958
- ...module.env
959
- };
960
- }, {});
1366
+ const envEntries = selectedModules.reduce(
1367
+ (acc, moduleId) => {
1368
+ const module = getModuleById(moduleId);
1369
+ return {
1370
+ ...acc,
1371
+ ...module.env
1372
+ };
1373
+ },
1374
+ {}
1375
+ );
961
1376
  if (Object.keys(envEntries).length > 0) {
962
- const envTargets = [
963
- ".env",
964
- ".env.example",
965
- ".env.development"
966
- ];
1377
+ const envTargets = [".env", ".env.example", ".env.development"];
967
1378
  for (const envFile of envTargets) {
968
- const envPath = path4.join(cwd, envFile);
1379
+ const envPath = path6.join(cwd, envFile);
969
1380
  if (await pathExists(envPath)) {
970
1381
  await mergeEnvFile(envPath, envEntries);
971
1382
  }
972
1383
  }
973
1384
  }
974
1385
  if (selectedModules.includes("chat")) {
975
- const envTargets = [
976
- ".env",
977
- ".env.example",
978
- ".env.development"
979
- ];
1386
+ const envTargets = [".env", ".env.example", ".env.development"];
980
1387
  for (const envFile of envTargets) {
981
- await upsertEnvValue(path4.join(cwd, envFile), "NEXT_PUBLIC_ENABLE_CHAT", "true");
1388
+ await upsertEnvValue(
1389
+ path6.join(cwd, envFile),
1390
+ "NEXT_PUBLIC_ENABLE_CHAT",
1391
+ "true"
1392
+ );
982
1393
  }
983
1394
  }
984
- const dependencyEntries = selectedModules.reduce((acc, moduleId) => {
985
- const module = getModuleById(moduleId);
986
- return {
987
- ...acc,
988
- ...module.dependencies ?? {}
989
- };
990
- }, {});
1395
+ const dependencyEntries = selectedModules.reduce(
1396
+ (acc, moduleId) => {
1397
+ const module = getModuleById(moduleId);
1398
+ return {
1399
+ ...acc,
1400
+ ...module.dependencies ?? {}
1401
+ };
1402
+ },
1403
+ {}
1404
+ );
991
1405
  if (Object.keys(dependencyEntries).length > 0) {
992
- await addDependencies(path4.join(cwd, "package.json"), dependencyEntries);
1406
+ await addDependencies(
1407
+ path6.join(cwd, "package.json"),
1408
+ dependencyEntries
1409
+ );
993
1410
  }
1411
+ const state = await detectProjectState(cwd);
1412
+ const namespaceSet = new Set(state.namespaces);
1413
+ for (const moduleId of selectedModules) {
1414
+ const moduleTemplateMessages = path6.join(
1415
+ getModuleById(moduleId).templatePath,
1416
+ "messages/vi"
1417
+ );
1418
+ if (!await pathExists(moduleTemplateMessages)) {
1419
+ continue;
1420
+ }
1421
+ const files = await readdir4(moduleTemplateMessages, {
1422
+ withFileTypes: true
1423
+ });
1424
+ for (const file of files) {
1425
+ if (!file.isFile() || !file.name.endsWith(".json")) {
1426
+ continue;
1427
+ }
1428
+ const namespace = file.name.replace(/\.json$/, "");
1429
+ namespaceSet.add(namespace);
1430
+ const templateData = JSON.parse(
1431
+ await readFile5(
1432
+ path6.join(moduleTemplateMessages, file.name),
1433
+ "utf8"
1434
+ )
1435
+ );
1436
+ await writeNamespaceMessages(cwd, namespace, templateData);
1437
+ await appendNamespace(cwd, namespace);
1438
+ }
1439
+ }
1440
+ await writeManifest(cwd, {
1441
+ ...state,
1442
+ namespaces: [...namespaceSet],
1443
+ modules: [.../* @__PURE__ */ new Set([...state.modules, ...selectedModules])]
1444
+ });
994
1445
  finishPrompt(`Added modules: ${selectedModules.join(", ")}`);
995
1446
  log.detail("Copied files", String(copiedFileCount));
996
1447
  if (autoAddedModules.length > 0) {
@@ -1011,18 +1462,92 @@ function registerAddCommand(program2) {
1011
1462
  }
1012
1463
  }
1013
1464
  if (chatSchemaStatus === "added") {
1014
- log.info("Optional chat schema block was appended to prisma/schema.prisma.");
1465
+ log.info(
1466
+ "Optional chat schema block was appended to prisma/schema.prisma."
1467
+ );
1468
+ }
1469
+ log.step(
1470
+ "Next: run your package manager install to apply new dependencies."
1471
+ );
1472
+ });
1473
+ 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
+ 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"));
1477
+ if (!hasMessages || !hasConfig) {
1478
+ log.error(
1479
+ "Run this command from a generated Next.js project with i18n scaffold."
1480
+ );
1481
+ process.exitCode = 1;
1482
+ return;
1483
+ }
1484
+ const state = await detectProjectState(cwd);
1485
+ const supportedLocales = [
1486
+ { id: "en", label: "English" },
1487
+ { id: "ja", label: "Japanese" },
1488
+ { id: "ko", label: "Korean" }
1489
+ ];
1490
+ const requested = options.locale ? options.locale.flatMap((value) => value.split(",")).map((value) => value.trim().toLowerCase()).filter(Boolean) : [];
1491
+ const preselected = requested.filter(
1492
+ (item) => supportedLocales.some((supported) => supported.id === item)
1493
+ );
1494
+ const available = supportedLocales.filter(
1495
+ (locale) => !state.locales.includes(locale.id)
1496
+ );
1497
+ if (available.length === 0) {
1498
+ log.info("All supported locales already exist.");
1499
+ return;
1015
1500
  }
1016
- log.step("Next: run your package manager install to apply new dependencies.");
1501
+ startPrompt("NexTCLI i18n language setup");
1502
+ const selected = preselected.length > 0 ? preselected : options.yes ? ["en"] : await askMultiSelect(
1503
+ "Select locales to add:",
1504
+ available.map((locale) => ({
1505
+ value: locale.id,
1506
+ label: locale.label,
1507
+ hint: locale.id === "en" ? "Required baseline locale" : "Optional locale"
1508
+ })),
1509
+ available.some((locale) => locale.id === "en") ? ["en"] : []
1510
+ );
1511
+ const normalized = [
1512
+ ...new Set(
1513
+ selected.filter(
1514
+ (value) => available.some((item) => item.id === value)
1515
+ )
1516
+ )
1517
+ ];
1518
+ if (!normalized.includes("en")) {
1519
+ normalized.unshift("en");
1520
+ }
1521
+ if (normalized.length === 0) {
1522
+ finishPrompt("No locales selected.");
1523
+ return;
1524
+ }
1525
+ for (const locale of normalized) {
1526
+ await cloneLocaleMessages(cwd, "vi", locale);
1527
+ }
1528
+ const mergedLocales = [
1529
+ .../* @__PURE__ */ new Set([...state.locales, ...normalized])
1530
+ ].sort();
1531
+ await patchLocalesConfig(cwd, mergedLocales);
1532
+ await writeManifest(cwd, {
1533
+ ...state,
1534
+ locales: mergedLocales
1535
+ });
1536
+ finishPrompt(`Added locales: ${normalized.join(", ")}`);
1537
+ log.info(
1538
+ "Locale files were cloned from Vietnamese values. Translate them when ready."
1539
+ );
1017
1540
  });
1018
1541
  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) => {
1019
1542
  const cwd = process.cwd();
1020
- const authFilePath = path4.join(cwd, "src/lib/auth.ts");
1021
- const hasSrc = await pathExists(path4.join(cwd, "src"));
1022
- const hasPackageJson = await pathExists(path4.join(cwd, "package.json"));
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"));
1023
1546
  const hasAuthFile = await pathExists(authFilePath);
1024
1547
  if (!hasSrc || !hasPackageJson || !hasAuthFile) {
1025
- log.error("Run this command from a generated Next.js project with src/lib/auth.ts.");
1548
+ log.error(
1549
+ "Run this command from a generated Next.js project with src/lib/auth.ts."
1550
+ );
1026
1551
  process.exitCode = 1;
1027
1552
  return;
1028
1553
  }
@@ -1039,8 +1564,16 @@ function registerAddCommand(program2) {
1039
1564
  const selectedProviders = requestedProviders.length > 0 ? [...new Set(requestedProviders)] : options.yes ? [] : await askMultiSelect(
1040
1565
  "Select social providers to enable:",
1041
1566
  [
1042
- { value: "google", label: "Google", hint: "Google OAuth login" },
1043
- { value: "facebook", label: "Facebook", hint: "Facebook OAuth login" }
1567
+ {
1568
+ value: "google",
1569
+ label: "Google",
1570
+ hint: "Google OAuth login"
1571
+ },
1572
+ {
1573
+ value: "facebook",
1574
+ label: "Facebook",
1575
+ hint: "Facebook OAuth login"
1576
+ }
1044
1577
  ],
1045
1578
  []
1046
1579
  );
@@ -1048,11 +1581,13 @@ function registerAddCommand(program2) {
1048
1581
  finishPrompt("No auth providers selected.");
1049
1582
  return;
1050
1583
  }
1051
- const authContent = await readFile3(authFilePath, "utf8");
1584
+ const authContent = await readFile5(authFilePath, "utf8");
1052
1585
  const existingProviders = readConfiguredProviders(authContent);
1053
- const mergedProviders = [.../* @__PURE__ */ new Set([...existingProviders, ...selectedProviders])];
1586
+ const mergedProviders = [
1587
+ .../* @__PURE__ */ new Set([...existingProviders, ...selectedProviders])
1588
+ ];
1054
1589
  const nextAuthContent = patchAuthProviders(authContent, mergedProviders);
1055
- await writeFile3(authFilePath, nextAuthContent, "utf8");
1590
+ await writeFile5(authFilePath, nextAuthContent, "utf8");
1056
1591
  const envEntries = {};
1057
1592
  if (mergedProviders.includes("google")) {
1058
1593
  envEntries.GOOGLE_CLIENT_ID = "";
@@ -1062,19 +1597,17 @@ function registerAddCommand(program2) {
1062
1597
  envEntries.FACEBOOK_CLIENT_ID = "";
1063
1598
  envEntries.FACEBOOK_CLIENT_SECRET = "";
1064
1599
  }
1065
- const envTargets = [
1066
- ".env",
1067
- ".env.example",
1068
- ".env.development"
1069
- ];
1600
+ const envTargets = [".env", ".env.example", ".env.development"];
1070
1601
  for (const envFile of envTargets) {
1071
- const envPath = path4.join(cwd, envFile);
1602
+ const envPath = path6.join(cwd, envFile);
1072
1603
  if (await pathExists(envPath)) {
1073
1604
  await mergeEnvFile(envPath, envEntries);
1074
1605
  }
1075
1606
  }
1076
1607
  finishPrompt(`Enabled providers: ${mergedProviders.join(", ")}`);
1077
- await ensureBetterAuthGenerate(cwd, { nonInteractive: Boolean(options.yes) });
1608
+ await ensureBetterAuthGenerate(cwd, {
1609
+ nonInteractive: Boolean(options.yes)
1610
+ });
1078
1611
  log.step("Next: set provider secrets in .env and restart dev server.");
1079
1612
  });
1080
1613
  }
@@ -1082,7 +1615,7 @@ function registerAddCommand(program2) {
1082
1615
  // src/commands/create.ts
1083
1616
  import { spawn as spawn2 } from "child_process";
1084
1617
  import { randomBytes } from "crypto";
1085
- import path5 from "path";
1618
+ import path7 from "path";
1086
1619
  async function runInstall(packageManager, cwd) {
1087
1620
  const installArgsMap = {
1088
1621
  npm: ["install"],
@@ -1138,7 +1671,7 @@ async function resolveProjectName() {
1138
1671
  }
1139
1672
  }
1140
1673
  });
1141
- const targetPath = path5.resolve(process.cwd(), projectName);
1674
+ const targetPath = path7.resolve(process.cwd(), projectName);
1142
1675
  if (await pathExists(targetPath)) {
1143
1676
  log.error(`Target directory already exists: ${targetPath}`);
1144
1677
  continue;
@@ -1150,8 +1683,8 @@ function registerCreateCommand(program2) {
1150
1683
  program2.command("create").description("Create a new outsource-ready Next.js app").action(async () => {
1151
1684
  startPrompt("NexTCLI project creation");
1152
1685
  const projectName = await resolveProjectName();
1153
- const targetPath = path5.resolve(process.cwd(), projectName);
1154
- const projectDirectoryName = path5.basename(targetPath);
1686
+ const targetPath = path7.resolve(process.cwd(), projectName);
1687
+ const projectDirectoryName = path7.basename(targetPath);
1155
1688
  const projectSlug = toProjectSlug(projectDirectoryName);
1156
1689
  const packageManager = await askSelect(
1157
1690
  "Which package manager do you want to use?",
@@ -1200,7 +1733,7 @@ function registerCreateCommand(program2) {
1200
1733
  if (Object.keys(moduleEnvEntries).length > 0) {
1201
1734
  const envTargets = [".env", ".env.example", ".env.development"];
1202
1735
  for (const envFile of envTargets) {
1203
- const envPath = path5.join(targetPath, envFile);
1736
+ const envPath = path7.join(targetPath, envFile);
1204
1737
  if (await pathExists(envPath)) {
1205
1738
  await mergeEnvFile(envPath, moduleEnvEntries);
1206
1739
  }
@@ -1218,7 +1751,7 @@ function registerCreateCommand(program2) {
1218
1751
  );
1219
1752
  if (Object.keys(dependencyEntries).length > 0) {
1220
1753
  await addDependencies(
1221
- path5.join(targetPath, "package.json"),
1754
+ path7.join(targetPath, "package.json"),
1222
1755
  dependencyEntries
1223
1756
  );
1224
1757
  }
@@ -1226,8 +1759,17 @@ function registerCreateCommand(program2) {
1226
1759
  await replaceTokensInDirectory(targetPath, {
1227
1760
  __PROJECT_NAME__: projectSlug,
1228
1761
  __ENABLE_CHAT__: selectedModules.includes("chat") ? "true" : "false",
1229
- __BETTER_AUTH_SECRET__: betterAuthSecret
1762
+ __BETTER_AUTH_SECRET__: betterAuthSecret,
1763
+ __NEXTCLI_VERSION__: "0.3.0"
1230
1764
  });
1765
+ const manifest = await readManifest(targetPath);
1766
+ if (manifest) {
1767
+ await writeManifest(targetPath, {
1768
+ ...manifest,
1769
+ cli: "0.3.0",
1770
+ modules: selectedModules
1771
+ });
1772
+ }
1231
1773
  if (shouldInstall) {
1232
1774
  log.step(`Installing dependencies with ${packageManager}...`);
1233
1775
  await runInstall(packageManager, targetPath);
@@ -1255,7 +1797,7 @@ function registerCreateCommand(program2) {
1255
1797
 
1256
1798
  // src/commands/migrate.ts
1257
1799
  import { spawn as spawn3 } from "child_process";
1258
- import path6 from "path";
1800
+ import path8 from "path";
1259
1801
  function createDefaultMigrationName() {
1260
1802
  const now = /* @__PURE__ */ new Date();
1261
1803
  const y = now.getFullYear();
@@ -1267,16 +1809,16 @@ function createDefaultMigrationName() {
1267
1809
  return `auto_${y}${m}${d}${hh}${mm}${ss}`;
1268
1810
  }
1269
1811
  async function detectPackageManager(cwd) {
1270
- if (await pathExists(path6.join(cwd, "bun.lockb"))) {
1812
+ if (await pathExists(path8.join(cwd, "bun.lockb"))) {
1271
1813
  return "bun";
1272
1814
  }
1273
- if (await pathExists(path6.join(cwd, "bun.lock"))) {
1815
+ if (await pathExists(path8.join(cwd, "bun.lock"))) {
1274
1816
  return "bun";
1275
1817
  }
1276
- if (await pathExists(path6.join(cwd, "pnpm-lock.yaml"))) {
1818
+ if (await pathExists(path8.join(cwd, "pnpm-lock.yaml"))) {
1277
1819
  return "pnpm";
1278
1820
  }
1279
- if (await pathExists(path6.join(cwd, "yarn.lock"))) {
1821
+ if (await pathExists(path8.join(cwd, "yarn.lock"))) {
1280
1822
  return "yarn";
1281
1823
  }
1282
1824
  return "npm";
@@ -1311,8 +1853,8 @@ async function runCommand2(command, args, cwd) {
1311
1853
  function registerMigrateCommand(program2) {
1312
1854
  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) => {
1313
1855
  const cwd = process.cwd();
1314
- const hasPackageJson = await pathExists(path6.join(cwd, "package.json"));
1315
- const hasPrismaSchema = await pathExists(path6.join(cwd, "prisma", "schema.prisma"));
1856
+ const hasPackageJson = await pathExists(path8.join(cwd, "package.json"));
1857
+ const hasPrismaSchema = await pathExists(path8.join(cwd, "prisma", "schema.prisma"));
1316
1858
  if (!hasPackageJson || !hasPrismaSchema) {
1317
1859
  log.error(
1318
1860
  "Run this command from a generated project root (requires package.json + prisma/schema.prisma)."
@@ -1464,7 +2006,7 @@ var NexTCLICommand = class _NexTCLICommand extends Command {
1464
2006
 
1465
2007
  // src/cli.ts
1466
2008
  var program = new NexTCLICommand();
1467
- program.name("nextcli").description("Scaffold outsource-ready Next.js projects").version("0.2.1");
2009
+ program.name("nextcli").description("Scaffold outsource-ready Next.js projects").version("0.3.0");
1468
2010
  registerCreateCommand(program);
1469
2011
  registerAddCommand(program);
1470
2012
  registerMigrateCommand(program);