@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.
- package/dist/cli.js +632 -90
- package/package.json +1 -1
- package/templates/next-base/components.json +21 -0
- package/templates/next-base/messages/vi/auth.json +28 -0
- package/templates/next-base/messages/vi/common.json +34 -0
- package/templates/next-base/messages/vi/example.json +10 -0
- package/templates/next-base/next.config.ts +11 -1
- package/templates/next-base/nextcli.json +8 -0
- package/templates/next-base/package.json +21 -1
- package/templates/next-base/postcss.config.mjs +5 -0
- package/templates/next-base/src/app/(auth)/layout.tsx +9 -0
- package/templates/next-base/src/app/(auth)/sign-in/page.tsx +6 -3
- package/templates/next-base/src/app/(dashboard)/account/page.tsx +9 -5
- package/templates/next-base/src/app/(dashboard)/dashboard/page.tsx +17 -0
- package/templates/next-base/src/app/(dashboard)/example/page.tsx +5 -2
- package/templates/next-base/src/app/(dashboard)/layout.tsx +10 -0
- package/templates/next-base/src/app/globals.css +107 -0
- package/templates/next-base/src/app/layout.tsx +18 -8
- package/templates/next-base/src/app/page.tsx +2 -18
- package/templates/next-base/src/components/layout/private/app-sidebar.tsx +45 -0
- package/templates/next-base/src/components/layout/private/dashboard-layout.tsx +53 -0
- package/templates/next-base/src/components/layout/private/locale-switcher.tsx +45 -0
- package/templates/next-base/src/components/layout/private/nav-sidebar.tsx +55 -0
- package/templates/next-base/src/components/layout/private/nav-user.tsx +99 -0
- package/templates/next-base/src/components/providers/theme-provider.tsx +11 -0
- package/templates/next-base/src/components/ui/alert-dialog.tsx +11 -0
- package/templates/next-base/src/components/ui/avatar.tsx +45 -0
- package/templates/next-base/src/components/ui/badge.tsx +29 -0
- package/templates/next-base/src/components/ui/button.tsx +47 -7
- package/templates/next-base/src/components/ui/card.tsx +54 -0
- package/templates/next-base/src/components/ui/data-table/data-table-column-header.tsx +23 -0
- package/templates/next-base/src/components/ui/data-table/data-table-filter-list.tsx +3 -0
- package/templates/next-base/src/components/ui/data-table/data-table-pagination.tsx +35 -0
- package/templates/next-base/src/components/ui/data-table/data-table-skeleton.tsx +11 -0
- package/templates/next-base/src/components/ui/data-table/data-table-toolbar.tsx +14 -0
- package/templates/next-base/src/components/ui/data-table/data-table-view-options.tsx +3 -0
- package/templates/next-base/src/components/ui/data-table/data-table.tsx +72 -0
- package/templates/next-base/src/components/ui/dialog.tsx +105 -0
- package/templates/next-base/src/components/ui/dropdown-menu.tsx +44 -0
- package/templates/next-base/src/components/ui/input.tsx +19 -0
- package/templates/next-base/src/components/ui/label.tsx +15 -0
- package/templates/next-base/src/components/ui/popover.tsx +30 -0
- package/templates/next-base/src/components/ui/scroll-area.tsx +47 -0
- package/templates/next-base/src/components/ui/select.tsx +76 -0
- package/templates/next-base/src/components/ui/separator.tsx +23 -0
- package/templates/next-base/src/components/ui/sheet.tsx +117 -0
- package/templates/next-base/src/components/ui/sidebar.tsx +215 -0
- package/templates/next-base/src/components/ui/skeleton.tsx +10 -0
- package/templates/next-base/src/components/ui/sonner.tsx +3 -0
- package/templates/next-base/src/components/ui/table.tsx +54 -0
- package/templates/next-base/src/components/ui/tabs.tsx +52 -0
- package/templates/next-base/src/components/ui/textarea.tsx +17 -0
- package/templates/next-base/src/components/ui/tooltip.tsx +26 -0
- package/templates/next-base/src/data/sidebar-modules.ts +11 -0
- package/templates/next-base/src/example/components/example-table.tsx +25 -40
- package/templates/next-base/src/features/auth/components/account-panel.tsx +21 -8
- package/templates/next-base/src/features/auth/components/sign-in-form.tsx +43 -30
- package/templates/next-base/src/hooks/index.ts +1 -1
- package/templates/next-base/src/hooks/table/use-data-table.ts +33 -0
- package/templates/next-base/src/hooks/use-mobile.ts +25 -0
- package/templates/next-base/src/i18n/config.ts +7 -0
- package/templates/next-base/src/i18n/namespaces.ts +5 -0
- package/templates/next-base/src/i18n/request.ts +19 -2
- package/templates/next-base/src/lib/prisma.ts +11 -1
- package/templates/next-base/src/types/data-table.ts +4 -0
- package/templates/next-base/src/types/index.ts +2 -0
- package/templates/next-base/middleware.ts +0 -10
- 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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
775
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
1208
|
+
const srcPath = path6.join(cwd, "src");
|
|
861
1209
|
if (!await pathExists(srcPath)) {
|
|
862
|
-
log.error(
|
|
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 =
|
|
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(
|
|
876
|
-
await ensureDir(
|
|
877
|
-
await
|
|
878
|
-
|
|
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
|
|
883
|
-
|
|
1232
|
+
await writeFile5(
|
|
1233
|
+
path6.join(featureRoot, "validations.ts"),
|
|
884
1234
|
buildFeatureValidationContent(modelPascal),
|
|
885
1235
|
"utf8"
|
|
886
1236
|
);
|
|
887
|
-
await
|
|
888
|
-
|
|
1237
|
+
await writeFile5(
|
|
1238
|
+
path6.join(featureRoot, "api", `use-${featureSlug}.ts`),
|
|
889
1239
|
buildFeatureHooksContent(featureSlug, modelPascal),
|
|
890
1240
|
"utf8"
|
|
891
1241
|
);
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
await
|
|
898
|
-
|
|
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(
|
|
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(
|
|
907
|
-
const hasPackageJson = await pathExists(
|
|
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(
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
1021
|
-
const hasSrc = await pathExists(
|
|
1022
|
-
const hasPackageJson = await pathExists(
|
|
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(
|
|
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
|
-
{
|
|
1043
|
-
|
|
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
|
|
1584
|
+
const authContent = await readFile5(authFilePath, "utf8");
|
|
1052
1585
|
const existingProviders = readConfiguredProviders(authContent);
|
|
1053
|
-
const mergedProviders = [
|
|
1586
|
+
const mergedProviders = [
|
|
1587
|
+
.../* @__PURE__ */ new Set([...existingProviders, ...selectedProviders])
|
|
1588
|
+
];
|
|
1054
1589
|
const nextAuthContent = patchAuthProviders(authContent, mergedProviders);
|
|
1055
|
-
await
|
|
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 =
|
|
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, {
|
|
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
|
|
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 =
|
|
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 =
|
|
1154
|
-
const projectDirectoryName =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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(
|
|
1812
|
+
if (await pathExists(path8.join(cwd, "bun.lockb"))) {
|
|
1271
1813
|
return "bun";
|
|
1272
1814
|
}
|
|
1273
|
-
if (await pathExists(
|
|
1815
|
+
if (await pathExists(path8.join(cwd, "bun.lock"))) {
|
|
1274
1816
|
return "bun";
|
|
1275
1817
|
}
|
|
1276
|
-
if (await pathExists(
|
|
1818
|
+
if (await pathExists(path8.join(cwd, "pnpm-lock.yaml"))) {
|
|
1277
1819
|
return "pnpm";
|
|
1278
1820
|
}
|
|
1279
|
-
if (await pathExists(
|
|
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(
|
|
1315
|
-
const hasPrismaSchema = await pathExists(
|
|
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.
|
|
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);
|