@thinhnguyencth1204/nextcli 0.2.1 → 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.
- package/README.md +6 -2
- package/dist/cli.js +778 -101
- package/package.json +2 -1
- package/templates/next-base/PROJECT_STRUCTURE.md +88 -0
- package/templates/next-base/SETUP.md +86 -0
- package/templates/next-base/bun.lock +1443 -0
- package/templates/next-base/components.json +21 -0
- package/templates/next-base/messages/vi/auth.json +42 -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-env.d.ts +3 -1
- 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/prisma/migrations/20260612000000_init/migration.sql +104 -0
- package/templates/next-base/prisma/migrations/migration_lock.toml +3 -0
- package/templates/next-base/prisma/schema.prisma +23 -9
- package/templates/next-base/public/logo.svg +4 -0
- package/templates/next-base/src/app/(auth)/change-password/layout.tsx +21 -0
- package/templates/next-base/src/app/(auth)/change-password/page.tsx +14 -0
- package/templates/next-base/src/app/(auth)/layout.tsx +9 -0
- package/templates/next-base/src/app/(auth)/sign-in/layout.tsx +17 -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 +22 -0
- package/templates/next-base/src/app/api/v1/auth/change-password/route.ts +55 -0
- package/templates/next-base/src/app/api/v1/auth/login/route.ts +15 -5
- package/templates/next-base/src/app/api/v1/auth/me/route.ts +17 -19
- package/templates/next-base/src/app/api/v1/users/[id]/route.ts +104 -0
- package/templates/next-base/src/app/api/v1/users/route.ts +58 -0
- package/templates/next-base/src/app/globals.css +111 -0
- package/templates/next-base/src/app/layout.tsx +24 -10
- package/templates/next-base/src/app/page.tsx +2 -18
- package/templates/next-base/src/components/branding/logo.tsx +27 -0
- package/templates/next-base/src/components/layout/private/app-sidebar.tsx +44 -0
- package/templates/next-base/src/components/layout/private/dashboard-layout.tsx +54 -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/config/branding.ts +14 -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 +32 -14
- package/templates/next-base/src/features/auth/components/change-password-form.tsx +82 -0
- package/templates/next-base/src/features/auth/components/sign-in-form.tsx +53 -35
- package/templates/next-base/src/features/auth/validations.ts +7 -1
- package/templates/next-base/src/features/users/services.ts +132 -0
- package/templates/next-base/src/features/users/validations.ts +21 -0
- 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/instrumentation.ts +14 -0
- package/templates/next-base/src/lib/auth-client.ts +2 -2
- package/templates/next-base/src/lib/auth.ts +2 -2
- package/templates/next-base/src/lib/bootstrap.ts +96 -0
- package/templates/next-base/src/lib/constants.ts +7 -0
- package/templates/next-base/src/lib/prisma.ts +11 -1
- package/templates/next-base/src/lib/rbac.ts +62 -0
- 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/tsconfig.json +29 -7
- 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 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, {
|
|
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(
|
|
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
|
-
|
|
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(
|
|
156
|
-
|
|
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 { readFile as
|
|
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) {
|
|
@@ -501,6 +546,263 @@ async function ensureChatSchemaInProject(projectRoot) {
|
|
|
501
546
|
return "added";
|
|
502
547
|
}
|
|
503
548
|
|
|
549
|
+
// src/core/i18n.ts
|
|
550
|
+
import path5 from "path";
|
|
551
|
+
import { readdir as readdir3, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
552
|
+
|
|
553
|
+
// src/core/manifest.ts
|
|
554
|
+
import path4 from "path";
|
|
555
|
+
import { readdir as readdir2, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
556
|
+
var defaultManifest = {
|
|
557
|
+
cli: "0.4.0",
|
|
558
|
+
defaultLocale: "vi",
|
|
559
|
+
locales: ["vi"],
|
|
560
|
+
namespaces: ["common", "auth", "example"],
|
|
561
|
+
modules: [],
|
|
562
|
+
features: ["example"]
|
|
563
|
+
};
|
|
564
|
+
function getManifestPath(projectDir) {
|
|
565
|
+
return path4.join(projectDir, "nextcli.json");
|
|
566
|
+
}
|
|
567
|
+
async function readManifest(projectDir) {
|
|
568
|
+
const manifestPath = getManifestPath(projectDir);
|
|
569
|
+
if (!await pathExists(manifestPath)) {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
const raw = await readFile3(manifestPath, "utf8");
|
|
573
|
+
return JSON.parse(raw);
|
|
574
|
+
}
|
|
575
|
+
async function writeManifest(projectDir, manifest) {
|
|
576
|
+
const manifestPath = getManifestPath(projectDir);
|
|
577
|
+
await writeFile3(
|
|
578
|
+
manifestPath,
|
|
579
|
+
`${JSON.stringify(manifest, null, 2)}
|
|
580
|
+
`,
|
|
581
|
+
"utf8"
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
function parseConstArray(content, marker) {
|
|
585
|
+
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;
|
|
586
|
+
const match = content.match(regex);
|
|
587
|
+
if (!match) {
|
|
588
|
+
return [];
|
|
589
|
+
}
|
|
590
|
+
return match[1].split(",").map((item) => item.trim().replaceAll('"', "").replaceAll("'", "")).filter(Boolean);
|
|
591
|
+
}
|
|
592
|
+
async function detectLocalesFromDisk(projectDir) {
|
|
593
|
+
const messagesDir = path4.join(projectDir, "messages");
|
|
594
|
+
if (!await pathExists(messagesDir)) {
|
|
595
|
+
return ["vi"];
|
|
596
|
+
}
|
|
597
|
+
const entries = await readdir2(messagesDir, { withFileTypes: true });
|
|
598
|
+
const locales = entries.filter((item) => item.isDirectory()).map((item) => item.name);
|
|
599
|
+
return locales.length > 0 ? locales.sort() : ["vi"];
|
|
600
|
+
}
|
|
601
|
+
async function detectNamespacesFromDisk(projectDir) {
|
|
602
|
+
const namespaceFile = path4.join(projectDir, "src/i18n/namespaces.ts");
|
|
603
|
+
if (!await pathExists(namespaceFile)) {
|
|
604
|
+
return [...defaultManifest.namespaces];
|
|
605
|
+
}
|
|
606
|
+
const content = await readFile3(namespaceFile, "utf8");
|
|
607
|
+
const namespaces = parseConstArray(content, "namespaces");
|
|
608
|
+
return namespaces.length > 0 ? namespaces : [...defaultManifest.namespaces];
|
|
609
|
+
}
|
|
610
|
+
async function reconcileManifest(projectDir) {
|
|
611
|
+
const localesFromDisk = await detectLocalesFromDisk(projectDir);
|
|
612
|
+
const namespacesFromDisk = await detectNamespacesFromDisk(projectDir);
|
|
613
|
+
const existing = await readManifest(projectDir);
|
|
614
|
+
const merged = {
|
|
615
|
+
...existing ?? defaultManifest,
|
|
616
|
+
locales: [.../* @__PURE__ */ new Set([...existing?.locales ?? [], ...localesFromDisk])],
|
|
617
|
+
namespaces: [
|
|
618
|
+
.../* @__PURE__ */ new Set([...existing?.namespaces ?? [], ...namespacesFromDisk])
|
|
619
|
+
]
|
|
620
|
+
};
|
|
621
|
+
merged.defaultLocale = merged.locales.includes(merged.defaultLocale) ? merged.defaultLocale : merged.locales[0] ?? "vi";
|
|
622
|
+
merged.modules = [...new Set(merged.modules)];
|
|
623
|
+
merged.features = [...new Set(merged.features)];
|
|
624
|
+
await writeManifest(projectDir, merged);
|
|
625
|
+
return merged;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// src/core/i18n.ts
|
|
629
|
+
var localeStartMarker = "// nextcli:locales:start";
|
|
630
|
+
var localeEndMarker = "// nextcli:locales:end";
|
|
631
|
+
var namespaceStartMarker = "// nextcli:namespaces:start";
|
|
632
|
+
var namespaceEndMarker = "// nextcli:namespaces:end";
|
|
633
|
+
function formatArray(items) {
|
|
634
|
+
return items.map((item) => `"${item}"`).join(", ");
|
|
635
|
+
}
|
|
636
|
+
function patchBetweenMarkers(content, start, end, replacement) {
|
|
637
|
+
const regex = new RegExp(`(${start})([\\s\\S]*?)(${end})`, "m");
|
|
638
|
+
return content.replace(regex, `$1
|
|
639
|
+
${replacement}
|
|
640
|
+
$3`);
|
|
641
|
+
}
|
|
642
|
+
function deepCloneValue(value) {
|
|
643
|
+
return JSON.parse(JSON.stringify(value));
|
|
644
|
+
}
|
|
645
|
+
async function detectProjectState(projectDir) {
|
|
646
|
+
return reconcileManifest(projectDir);
|
|
647
|
+
}
|
|
648
|
+
async function patchLocalesConfig(projectDir, locales) {
|
|
649
|
+
const configPath = path5.join(projectDir, "src/i18n/config.ts");
|
|
650
|
+
if (!await pathExists(configPath)) {
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
const content = await readFile4(configPath, "utf8");
|
|
654
|
+
const next = patchBetweenMarkers(
|
|
655
|
+
content,
|
|
656
|
+
localeStartMarker,
|
|
657
|
+
localeEndMarker,
|
|
658
|
+
`export const locales = [${formatArray(locales)}] as const;`
|
|
659
|
+
);
|
|
660
|
+
await writeFile4(configPath, next, "utf8");
|
|
661
|
+
}
|
|
662
|
+
async function appendNamespace(projectDir, namespace) {
|
|
663
|
+
const namespacePath = path5.join(projectDir, "src/i18n/namespaces.ts");
|
|
664
|
+
if (!await pathExists(namespacePath)) {
|
|
665
|
+
return [];
|
|
666
|
+
}
|
|
667
|
+
const currentState = await detectProjectState(projectDir);
|
|
668
|
+
const namespaces = [.../* @__PURE__ */ new Set([...currentState.namespaces, namespace])];
|
|
669
|
+
const content = await readFile4(namespacePath, "utf8");
|
|
670
|
+
const next = patchBetweenMarkers(
|
|
671
|
+
content,
|
|
672
|
+
namespaceStartMarker,
|
|
673
|
+
namespaceEndMarker,
|
|
674
|
+
`export const namespaces = [${formatArray(namespaces)}] as const;`
|
|
675
|
+
);
|
|
676
|
+
await writeFile4(namespacePath, next, "utf8");
|
|
677
|
+
await writeManifest(projectDir, {
|
|
678
|
+
...currentState,
|
|
679
|
+
namespaces
|
|
680
|
+
});
|
|
681
|
+
return namespaces;
|
|
682
|
+
}
|
|
683
|
+
async function cloneLocaleMessages(projectDir, fromLocale, toLocale) {
|
|
684
|
+
const sourceDir = path5.join(projectDir, "messages", fromLocale);
|
|
685
|
+
const targetDir = path5.join(projectDir, "messages", toLocale);
|
|
686
|
+
if (!await pathExists(sourceDir)) {
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const entries = await readdir3(sourceDir, { withFileTypes: true });
|
|
690
|
+
if (!await pathExists(targetDir)) {
|
|
691
|
+
await ensureDir(targetDir);
|
|
692
|
+
}
|
|
693
|
+
for (const entry of entries) {
|
|
694
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) {
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
const sourcePath = path5.join(sourceDir, entry.name);
|
|
698
|
+
const targetPath = path5.join(targetDir, entry.name);
|
|
699
|
+
const sourceContent = JSON.parse(
|
|
700
|
+
await readFile4(sourcePath, "utf8")
|
|
701
|
+
);
|
|
702
|
+
await writeFile4(
|
|
703
|
+
targetPath,
|
|
704
|
+
`${JSON.stringify(deepCloneValue(sourceContent), null, 2)}
|
|
705
|
+
`,
|
|
706
|
+
"utf8"
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
async function writeNamespaceMessages(projectDir, namespace, viTemplate) {
|
|
711
|
+
const state = await detectProjectState(projectDir);
|
|
712
|
+
for (const locale of state.locales) {
|
|
713
|
+
const localeDir = path5.join(projectDir, "messages", locale);
|
|
714
|
+
if (!await pathExists(localeDir)) {
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
const filePath = path5.join(localeDir, `${namespace}.json`);
|
|
718
|
+
await writeFile4(
|
|
719
|
+
filePath,
|
|
720
|
+
`${JSON.stringify(deepCloneValue(viTemplate), null, 2)}
|
|
721
|
+
`,
|
|
722
|
+
"utf8"
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
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
|
+
|
|
504
806
|
// src/commands/add.ts
|
|
505
807
|
function toKebabCase(input) {
|
|
506
808
|
return input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -654,6 +956,172 @@ export function useDelete${modelPascal}() {
|
|
|
654
956
|
}
|
|
655
957
|
`;
|
|
656
958
|
}
|
|
959
|
+
function buildFeatureTableContent(featureSlug, modelPascal) {
|
|
960
|
+
return `"use client";
|
|
961
|
+
|
|
962
|
+
import { useMemo } from "react";
|
|
963
|
+
import { useTranslations } from "next-intl";
|
|
964
|
+
import { createColumnHelper, getCoreRowModel, getPaginationRowModel, useReactTable } from "@tanstack/react-table";
|
|
965
|
+
import { DataTable } from "@/components/ui/data-table/data-table";
|
|
966
|
+
import { use${modelPascal}s } from "@/features/${featureSlug}/api/use-${featureSlug}";
|
|
967
|
+
|
|
968
|
+
type ${modelPascal}Item = {
|
|
969
|
+
id: string;
|
|
970
|
+
name: string;
|
|
971
|
+
description?: string | null;
|
|
972
|
+
createdAt: string;
|
|
973
|
+
updatedAt: string;
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
const columnHelper = createColumnHelper<${modelPascal}Item>();
|
|
977
|
+
|
|
978
|
+
export function ${modelPascal}Table() {
|
|
979
|
+
const t = useTranslations("${featureSlug}");
|
|
980
|
+
const { data, isLoading } = use${modelPascal}s();
|
|
981
|
+
|
|
982
|
+
const columns = useMemo(
|
|
983
|
+
() => [
|
|
984
|
+
columnHelper.accessor("name", {
|
|
985
|
+
header: t("table.name"),
|
|
986
|
+
}),
|
|
987
|
+
columnHelper.accessor("description", {
|
|
988
|
+
header: t("table.description"),
|
|
989
|
+
}),
|
|
990
|
+
],
|
|
991
|
+
[t],
|
|
992
|
+
);
|
|
993
|
+
|
|
994
|
+
const table = useReactTable({
|
|
995
|
+
data: Array.isArray(data) ? data : [],
|
|
996
|
+
columns,
|
|
997
|
+
getCoreRowModel: getCoreRowModel(),
|
|
998
|
+
getPaginationRowModel: getPaginationRowModel(),
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
if (isLoading) {
|
|
1002
|
+
return <p>{t("table.loading")}</p>;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
return <DataTable table={table} />;
|
|
1006
|
+
}
|
|
1007
|
+
`;
|
|
1008
|
+
}
|
|
1009
|
+
function buildFeatureDialogContent(featureSlug, modelPascal) {
|
|
1010
|
+
return `"use client";
|
|
1011
|
+
|
|
1012
|
+
import { useState, type FormEvent } from "react";
|
|
1013
|
+
import { useTranslations } from "next-intl";
|
|
1014
|
+
import { Button } from "@/components/ui/button";
|
|
1015
|
+
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
|
1016
|
+
import { Input } from "@/components/ui/input";
|
|
1017
|
+
import { Label } from "@/components/ui/label";
|
|
1018
|
+
|
|
1019
|
+
export function Create${modelPascal}Dialog({
|
|
1020
|
+
onCreate,
|
|
1021
|
+
}: {
|
|
1022
|
+
onCreate: (payload: { name: string; description?: string }) => Promise<void>;
|
|
1023
|
+
}) {
|
|
1024
|
+
const t = useTranslations("${featureSlug}");
|
|
1025
|
+
const [open, setOpen] = useState(false);
|
|
1026
|
+
const [name, setName] = useState("");
|
|
1027
|
+
const [description, setDescription] = useState("");
|
|
1028
|
+
const [submitting, setSubmitting] = useState(false);
|
|
1029
|
+
|
|
1030
|
+
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
|
1031
|
+
event.preventDefault();
|
|
1032
|
+
setSubmitting(true);
|
|
1033
|
+
try {
|
|
1034
|
+
await onCreate({ name, description: description || undefined });
|
|
1035
|
+
setName("");
|
|
1036
|
+
setDescription("");
|
|
1037
|
+
setOpen(false);
|
|
1038
|
+
} finally {
|
|
1039
|
+
setSubmitting(false);
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
|
|
1043
|
+
return (
|
|
1044
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
1045
|
+
<DialogTrigger asChild>
|
|
1046
|
+
<Button>{t("dialog.open")}</Button>
|
|
1047
|
+
</DialogTrigger>
|
|
1048
|
+
<DialogContent>
|
|
1049
|
+
<DialogHeader>
|
|
1050
|
+
<DialogTitle>{t("dialog.title")}</DialogTitle>
|
|
1051
|
+
</DialogHeader>
|
|
1052
|
+
<form className="grid gap-4" onSubmit={handleSubmit}>
|
|
1053
|
+
<div className="grid gap-2">
|
|
1054
|
+
<Label htmlFor="name">{t("dialog.name")}</Label>
|
|
1055
|
+
<Input id="name" value={name} onChange={(event) => setName(event.target.value)} required />
|
|
1056
|
+
</div>
|
|
1057
|
+
<div className="grid gap-2">
|
|
1058
|
+
<Label htmlFor="description">{t("dialog.description")}</Label>
|
|
1059
|
+
<Input
|
|
1060
|
+
id="description"
|
|
1061
|
+
value={description}
|
|
1062
|
+
onChange={(event) => setDescription(event.target.value)}
|
|
1063
|
+
/>
|
|
1064
|
+
</div>
|
|
1065
|
+
<DialogFooter>
|
|
1066
|
+
<Button type="submit" disabled={submitting}>
|
|
1067
|
+
{submitting ? t("dialog.submitting") : t("dialog.submit")}
|
|
1068
|
+
</Button>
|
|
1069
|
+
</DialogFooter>
|
|
1070
|
+
</form>
|
|
1071
|
+
</DialogContent>
|
|
1072
|
+
</Dialog>
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
`;
|
|
1076
|
+
}
|
|
1077
|
+
function buildFeaturePageContent(featureSlug, modelPascal) {
|
|
1078
|
+
return `"use client";
|
|
1079
|
+
|
|
1080
|
+
import { useTranslations } from "next-intl";
|
|
1081
|
+
import { useCreate${modelPascal} } from "@/features/${featureSlug}/api/use-${featureSlug}";
|
|
1082
|
+
import { Create${modelPascal}Dialog } from "@/features/${featureSlug}/components/create-${featureSlug}-dialog";
|
|
1083
|
+
import { ${modelPascal}Table } from "@/features/${featureSlug}/components/${featureSlug}-table";
|
|
1084
|
+
|
|
1085
|
+
export default function ${modelPascal}Page() {
|
|
1086
|
+
const t = useTranslations("${featureSlug}");
|
|
1087
|
+
const createMutation = useCreate${modelPascal}();
|
|
1088
|
+
|
|
1089
|
+
return (
|
|
1090
|
+
<main className="space-y-4">
|
|
1091
|
+
<div className="flex items-center justify-between">
|
|
1092
|
+
<h1 className="text-2xl font-semibold">{t("page.title")}</h1>
|
|
1093
|
+
<Create${modelPascal}Dialog
|
|
1094
|
+
onCreate={async (payload) => {
|
|
1095
|
+
await createMutation.mutateAsync(payload);
|
|
1096
|
+
}}
|
|
1097
|
+
/>
|
|
1098
|
+
</div>
|
|
1099
|
+
<${modelPascal}Table />
|
|
1100
|
+
</main>
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
`;
|
|
1104
|
+
}
|
|
1105
|
+
function buildFeatureMessages(featureName) {
|
|
1106
|
+
return {
|
|
1107
|
+
page: {
|
|
1108
|
+
title: featureName
|
|
1109
|
+
},
|
|
1110
|
+
table: {
|
|
1111
|
+
name: "T\xEAn",
|
|
1112
|
+
description: "M\xF4 t\u1EA3",
|
|
1113
|
+
loading: "\u0110ang t\u1EA3i d\u1EEF li\u1EC7u..."
|
|
1114
|
+
},
|
|
1115
|
+
dialog: {
|
|
1116
|
+
open: "T\u1EA1o m\u1EDBi",
|
|
1117
|
+
title: `T\u1EA1o ${featureName}`,
|
|
1118
|
+
name: "T\xEAn",
|
|
1119
|
+
description: "M\xF4 t\u1EA3",
|
|
1120
|
+
submit: "L\u01B0u",
|
|
1121
|
+
submitting: "\u0110ang l\u01B0u..."
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
657
1125
|
function buildCollectionRouteContent(featureSlug, modelPascal) {
|
|
658
1126
|
return `import { fail, ok } from "@/lib/api-response";
|
|
659
1127
|
import {
|
|
@@ -750,11 +1218,11 @@ export async function DELETE(
|
|
|
750
1218
|
`;
|
|
751
1219
|
}
|
|
752
1220
|
async function appendFeatureModelToPrismaSchema(cwd, modelPascal) {
|
|
753
|
-
const schemaPath =
|
|
1221
|
+
const schemaPath = path7.join(cwd, "prisma", "schema.prisma");
|
|
754
1222
|
if (!await pathExists(schemaPath)) {
|
|
755
1223
|
return "skipped";
|
|
756
1224
|
}
|
|
757
|
-
const schemaContent = await
|
|
1225
|
+
const schemaContent = await readFile6(schemaPath, "utf8");
|
|
758
1226
|
const modelRegex = new RegExp(`\\bmodel\\s+${modelPascal}\\b`);
|
|
759
1227
|
if (modelRegex.test(schemaContent)) {
|
|
760
1228
|
return "exists";
|
|
@@ -771,8 +1239,12 @@ model ${modelPascal} {
|
|
|
771
1239
|
updatedAt DateTime @updatedAt
|
|
772
1240
|
}
|
|
773
1241
|
`;
|
|
774
|
-
await
|
|
775
|
-
|
|
1242
|
+
await writeFile6(
|
|
1243
|
+
schemaPath,
|
|
1244
|
+
`${schemaContent.trimEnd()}${modelBlock}
|
|
1245
|
+
`,
|
|
1246
|
+
"utf8"
|
|
1247
|
+
);
|
|
776
1248
|
return "added";
|
|
777
1249
|
}
|
|
778
1250
|
var authProviderStartMarker = "// AUTO_GENERATED_AUTH_PROVIDERS_START";
|
|
@@ -833,18 +1305,18 @@ async function upsertEnvValue(envFilePath, key, value) {
|
|
|
833
1305
|
if (!await pathExists(envFilePath)) {
|
|
834
1306
|
return;
|
|
835
1307
|
}
|
|
836
|
-
const content = await
|
|
1308
|
+
const content = await readFile6(envFilePath, "utf8");
|
|
837
1309
|
const entry = `${key}=${value}`;
|
|
838
1310
|
const pattern = new RegExp(`^${key}=.*$`, "m");
|
|
839
1311
|
if (pattern.test(content)) {
|
|
840
1312
|
const next = content.replace(pattern, entry);
|
|
841
1313
|
if (next !== content) {
|
|
842
|
-
await
|
|
1314
|
+
await writeFile6(envFilePath, next, "utf8");
|
|
843
1315
|
}
|
|
844
1316
|
return;
|
|
845
1317
|
}
|
|
846
1318
|
const separator = content.endsWith("\n") || content.length === 0 ? "" : "\n";
|
|
847
|
-
await
|
|
1319
|
+
await writeFile6(envFilePath, `${content}${separator}${entry}
|
|
848
1320
|
`, "utf8");
|
|
849
1321
|
}
|
|
850
1322
|
function registerAddCommand(program2) {
|
|
@@ -857,54 +1329,118 @@ function registerAddCommand(program2) {
|
|
|
857
1329
|
return;
|
|
858
1330
|
}
|
|
859
1331
|
const cwd = process.cwd();
|
|
860
|
-
const srcPath =
|
|
1332
|
+
const srcPath = path7.join(cwd, "src");
|
|
861
1333
|
if (!await pathExists(srcPath)) {
|
|
862
|
-
log.error(
|
|
1334
|
+
log.error(
|
|
1335
|
+
"Run this command from your generated Next.js project root (missing ./src)."
|
|
1336
|
+
);
|
|
863
1337
|
process.exitCode = 1;
|
|
864
1338
|
return;
|
|
865
1339
|
}
|
|
866
1340
|
const featurePascal = toPascalCase(featureSlug);
|
|
867
1341
|
const modelPascal = singularizeWord(featurePascal);
|
|
868
1342
|
const modelDelegate = toCamelCase(modelPascal);
|
|
869
|
-
const featureRoot =
|
|
1343
|
+
const featureRoot = path7.join(cwd, "src/features", featureSlug);
|
|
870
1344
|
if (await pathExists(featureRoot)) {
|
|
871
1345
|
log.error(`Feature already exists: ${featureRoot}`);
|
|
872
1346
|
process.exitCode = 1;
|
|
873
1347
|
return;
|
|
874
1348
|
}
|
|
875
|
-
await ensureDir(
|
|
876
|
-
await ensureDir(
|
|
877
|
-
await
|
|
878
|
-
|
|
1349
|
+
await ensureDir(path7.join(featureRoot, "api"));
|
|
1350
|
+
await ensureDir(path7.join(featureRoot, "components"));
|
|
1351
|
+
await writeFile6(
|
|
1352
|
+
path7.join(featureRoot, "services.ts"),
|
|
879
1353
|
buildFeatureServicesContent(modelPascal, modelDelegate),
|
|
880
1354
|
"utf8"
|
|
881
1355
|
);
|
|
882
|
-
await
|
|
883
|
-
|
|
1356
|
+
await writeFile6(
|
|
1357
|
+
path7.join(featureRoot, "validations.ts"),
|
|
884
1358
|
buildFeatureValidationContent(modelPascal),
|
|
885
1359
|
"utf8"
|
|
886
1360
|
);
|
|
887
|
-
await
|
|
888
|
-
|
|
1361
|
+
await writeFile6(
|
|
1362
|
+
path7.join(featureRoot, "api", `use-${featureSlug}.ts`),
|
|
889
1363
|
buildFeatureHooksContent(featureSlug, modelPascal),
|
|
890
1364
|
"utf8"
|
|
891
1365
|
);
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
await
|
|
898
|
-
|
|
1366
|
+
await writeFile6(
|
|
1367
|
+
path7.join(featureRoot, "components", `${featureSlug}-table.tsx`),
|
|
1368
|
+
buildFeatureTableContent(featureSlug, modelPascal),
|
|
1369
|
+
"utf8"
|
|
1370
|
+
);
|
|
1371
|
+
await writeFile6(
|
|
1372
|
+
path7.join(
|
|
1373
|
+
featureRoot,
|
|
1374
|
+
"components",
|
|
1375
|
+
`create-${featureSlug}-dialog.tsx`
|
|
1376
|
+
),
|
|
1377
|
+
buildFeatureDialogContent(featureSlug, modelPascal),
|
|
1378
|
+
"utf8"
|
|
1379
|
+
);
|
|
1380
|
+
const routeFilePath = path7.join(
|
|
1381
|
+
cwd,
|
|
1382
|
+
"src/app/api/v1",
|
|
1383
|
+
featureSlug,
|
|
1384
|
+
"route.ts"
|
|
1385
|
+
);
|
|
1386
|
+
await ensureDir(path7.dirname(routeFilePath));
|
|
1387
|
+
await writeFile6(
|
|
1388
|
+
routeFilePath,
|
|
1389
|
+
buildCollectionRouteContent(featureSlug, modelPascal),
|
|
1390
|
+
"utf8"
|
|
1391
|
+
);
|
|
1392
|
+
const idRoutePath = path7.join(
|
|
1393
|
+
cwd,
|
|
1394
|
+
"src/app/api/v1",
|
|
1395
|
+
featureSlug,
|
|
1396
|
+
"[id]",
|
|
1397
|
+
"route.ts"
|
|
1398
|
+
);
|
|
1399
|
+
await ensureDir(path7.dirname(idRoutePath));
|
|
1400
|
+
await writeFile6(
|
|
1401
|
+
idRoutePath,
|
|
1402
|
+
buildItemRouteContent(featureSlug, modelPascal),
|
|
1403
|
+
"utf8"
|
|
1404
|
+
);
|
|
1405
|
+
const featurePagePath = path7.join(
|
|
1406
|
+
cwd,
|
|
1407
|
+
"src/app/(dashboard)",
|
|
1408
|
+
featureSlug,
|
|
1409
|
+
"page.tsx"
|
|
1410
|
+
);
|
|
1411
|
+
await ensureDir(path7.dirname(featurePagePath));
|
|
1412
|
+
await writeFile6(
|
|
1413
|
+
featurePagePath,
|
|
1414
|
+
buildFeaturePageContent(featureSlug, modelPascal),
|
|
1415
|
+
"utf8"
|
|
1416
|
+
);
|
|
1417
|
+
const manifestState = await detectProjectState(cwd);
|
|
1418
|
+
await writeNamespaceMessages(
|
|
1419
|
+
cwd,
|
|
1420
|
+
featureSlug,
|
|
1421
|
+
buildFeatureMessages(modelPascal)
|
|
1422
|
+
);
|
|
1423
|
+
const namespaces = await appendNamespace(cwd, featureSlug);
|
|
1424
|
+
await writeManifest(cwd, {
|
|
1425
|
+
...manifestState,
|
|
1426
|
+
namespaces,
|
|
1427
|
+
features: [.../* @__PURE__ */ new Set([...manifestState.features, featureSlug])]
|
|
1428
|
+
});
|
|
1429
|
+
const schemaStatus = await appendFeatureModelToPrismaSchema(
|
|
1430
|
+
cwd,
|
|
1431
|
+
modelPascal
|
|
1432
|
+
);
|
|
899
1433
|
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
1434
|
log.success(`Feature generated with CRUD: src/features/${featureSlug}`);
|
|
901
1435
|
log.info(schemaMessage);
|
|
902
|
-
log.warn(
|
|
1436
|
+
log.warn(
|
|
1437
|
+
"No migration was executed. Run your migration command manually when ready."
|
|
1438
|
+
);
|
|
903
1439
|
});
|
|
904
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) => {
|
|
905
1441
|
const cwd = process.cwd();
|
|
906
|
-
const hasSrc = await pathExists(
|
|
907
|
-
const hasPackageJson = await pathExists(
|
|
1442
|
+
const hasSrc = await pathExists(path7.join(cwd, "src"));
|
|
1443
|
+
const hasPackageJson = await pathExists(path7.join(cwd, "package.json"));
|
|
908
1444
|
if (!hasSrc || !hasPackageJson) {
|
|
909
1445
|
log.error("Run this command from your generated Next.js project root.");
|
|
910
1446
|
process.exitCode = 1;
|
|
@@ -951,46 +1487,89 @@ function registerAddCommand(program2) {
|
|
|
951
1487
|
if (selectedModules.includes("chat")) {
|
|
952
1488
|
chatSchemaStatus = await ensureChatSchemaInProject(cwd);
|
|
953
1489
|
}
|
|
954
|
-
const envEntries = selectedModules.reduce(
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
1490
|
+
const envEntries = selectedModules.reduce(
|
|
1491
|
+
(acc, moduleId) => {
|
|
1492
|
+
const module = getModuleById(moduleId);
|
|
1493
|
+
return {
|
|
1494
|
+
...acc,
|
|
1495
|
+
...module.env
|
|
1496
|
+
};
|
|
1497
|
+
},
|
|
1498
|
+
{}
|
|
1499
|
+
);
|
|
961
1500
|
if (Object.keys(envEntries).length > 0) {
|
|
962
|
-
const envTargets = [
|
|
963
|
-
".env",
|
|
964
|
-
".env.example",
|
|
965
|
-
".env.development"
|
|
966
|
-
];
|
|
1501
|
+
const envTargets = [".env", ".env.example", ".env.development"];
|
|
967
1502
|
for (const envFile of envTargets) {
|
|
968
|
-
const envPath =
|
|
1503
|
+
const envPath = path7.join(cwd, envFile);
|
|
969
1504
|
if (await pathExists(envPath)) {
|
|
970
1505
|
await mergeEnvFile(envPath, envEntries);
|
|
971
1506
|
}
|
|
972
1507
|
}
|
|
973
1508
|
}
|
|
974
1509
|
if (selectedModules.includes("chat")) {
|
|
975
|
-
const envTargets = [
|
|
976
|
-
".env",
|
|
977
|
-
".env.example",
|
|
978
|
-
".env.development"
|
|
979
|
-
];
|
|
1510
|
+
const envTargets = [".env", ".env.example", ".env.development"];
|
|
980
1511
|
for (const envFile of envTargets) {
|
|
981
|
-
await upsertEnvValue(
|
|
1512
|
+
await upsertEnvValue(
|
|
1513
|
+
path7.join(cwd, envFile),
|
|
1514
|
+
"NEXT_PUBLIC_ENABLE_CHAT",
|
|
1515
|
+
"true"
|
|
1516
|
+
);
|
|
982
1517
|
}
|
|
983
1518
|
}
|
|
984
|
-
const dependencyEntries = selectedModules.reduce(
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1519
|
+
const dependencyEntries = selectedModules.reduce(
|
|
1520
|
+
(acc, moduleId) => {
|
|
1521
|
+
const module = getModuleById(moduleId);
|
|
1522
|
+
return {
|
|
1523
|
+
...acc,
|
|
1524
|
+
...module.dependencies ?? {}
|
|
1525
|
+
};
|
|
1526
|
+
},
|
|
1527
|
+
{}
|
|
1528
|
+
);
|
|
991
1529
|
if (Object.keys(dependencyEntries).length > 0) {
|
|
992
|
-
await addDependencies(
|
|
1530
|
+
await addDependencies(
|
|
1531
|
+
path7.join(cwd, "package.json"),
|
|
1532
|
+
dependencyEntries
|
|
1533
|
+
);
|
|
993
1534
|
}
|
|
1535
|
+
const state = await detectProjectState(cwd);
|
|
1536
|
+
const namespaceSet = new Set(state.namespaces);
|
|
1537
|
+
for (const moduleId of selectedModules) {
|
|
1538
|
+
const moduleTemplateMessages = path7.join(
|
|
1539
|
+
getModuleById(moduleId).templatePath,
|
|
1540
|
+
"messages/vi"
|
|
1541
|
+
);
|
|
1542
|
+
if (!await pathExists(moduleTemplateMessages)) {
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
const files = await readdir4(moduleTemplateMessages, {
|
|
1546
|
+
withFileTypes: true
|
|
1547
|
+
});
|
|
1548
|
+
for (const file of files) {
|
|
1549
|
+
if (!file.isFile() || !file.name.endsWith(".json")) {
|
|
1550
|
+
continue;
|
|
1551
|
+
}
|
|
1552
|
+
const namespace = file.name.replace(/\.json$/, "");
|
|
1553
|
+
namespaceSet.add(namespace);
|
|
1554
|
+
const templateData = JSON.parse(
|
|
1555
|
+
await readFile6(
|
|
1556
|
+
path7.join(moduleTemplateMessages, file.name),
|
|
1557
|
+
"utf8"
|
|
1558
|
+
)
|
|
1559
|
+
);
|
|
1560
|
+
await writeNamespaceMessages(cwd, namespace, templateData);
|
|
1561
|
+
await appendNamespace(cwd, namespace);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
await writeManifest(cwd, {
|
|
1565
|
+
...state,
|
|
1566
|
+
namespaces: [...namespaceSet],
|
|
1567
|
+
modules: [.../* @__PURE__ */ new Set([...state.modules, ...selectedModules])]
|
|
1568
|
+
});
|
|
1569
|
+
const mergedModules = [
|
|
1570
|
+
.../* @__PURE__ */ new Set([...state.modules, ...selectedModules])
|
|
1571
|
+
];
|
|
1572
|
+
await mergeModuleSetupSections(cwd, selectedModules, mergedModules);
|
|
994
1573
|
finishPrompt(`Added modules: ${selectedModules.join(", ")}`);
|
|
995
1574
|
log.detail("Copied files", String(copiedFileCount));
|
|
996
1575
|
if (autoAddedModules.length > 0) {
|
|
@@ -1011,18 +1590,92 @@ function registerAddCommand(program2) {
|
|
|
1011
1590
|
}
|
|
1012
1591
|
}
|
|
1013
1592
|
if (chatSchemaStatus === "added") {
|
|
1014
|
-
log.info(
|
|
1593
|
+
log.info(
|
|
1594
|
+
"Optional chat schema block was appended to prisma/schema.prisma."
|
|
1595
|
+
);
|
|
1596
|
+
}
|
|
1597
|
+
log.step(
|
|
1598
|
+
"Next: run your package manager install to apply new dependencies."
|
|
1599
|
+
);
|
|
1600
|
+
});
|
|
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) => {
|
|
1602
|
+
const cwd = process.cwd();
|
|
1603
|
+
const hasMessages = await pathExists(path7.join(cwd, "messages"));
|
|
1604
|
+
const hasConfig = await pathExists(path7.join(cwd, "src/i18n/config.ts"));
|
|
1605
|
+
if (!hasMessages || !hasConfig) {
|
|
1606
|
+
log.error(
|
|
1607
|
+
"Run this command from a generated Next.js project with i18n scaffold."
|
|
1608
|
+
);
|
|
1609
|
+
process.exitCode = 1;
|
|
1610
|
+
return;
|
|
1015
1611
|
}
|
|
1016
|
-
|
|
1612
|
+
const state = await detectProjectState(cwd);
|
|
1613
|
+
const supportedLocales = [
|
|
1614
|
+
{ id: "en", label: "English" },
|
|
1615
|
+
{ id: "ja", label: "Japanese" },
|
|
1616
|
+
{ id: "ko", label: "Korean" }
|
|
1617
|
+
];
|
|
1618
|
+
const requested = options.locale ? options.locale.flatMap((value) => value.split(",")).map((value) => value.trim().toLowerCase()).filter(Boolean) : [];
|
|
1619
|
+
const preselected = requested.filter(
|
|
1620
|
+
(item) => supportedLocales.some((supported) => supported.id === item)
|
|
1621
|
+
);
|
|
1622
|
+
const available = supportedLocales.filter(
|
|
1623
|
+
(locale) => !state.locales.includes(locale.id)
|
|
1624
|
+
);
|
|
1625
|
+
if (available.length === 0) {
|
|
1626
|
+
log.info("All supported locales already exist.");
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
startPrompt("NexTCLI i18n language setup");
|
|
1630
|
+
const selected = preselected.length > 0 ? preselected : options.yes ? ["en"] : await askMultiSelect(
|
|
1631
|
+
"Select locales to add:",
|
|
1632
|
+
available.map((locale) => ({
|
|
1633
|
+
value: locale.id,
|
|
1634
|
+
label: locale.label,
|
|
1635
|
+
hint: locale.id === "en" ? "Required baseline locale" : "Optional locale"
|
|
1636
|
+
})),
|
|
1637
|
+
available.some((locale) => locale.id === "en") ? ["en"] : []
|
|
1638
|
+
);
|
|
1639
|
+
const normalized = [
|
|
1640
|
+
...new Set(
|
|
1641
|
+
selected.filter(
|
|
1642
|
+
(value) => available.some((item) => item.id === value)
|
|
1643
|
+
)
|
|
1644
|
+
)
|
|
1645
|
+
];
|
|
1646
|
+
if (!normalized.includes("en")) {
|
|
1647
|
+
normalized.unshift("en");
|
|
1648
|
+
}
|
|
1649
|
+
if (normalized.length === 0) {
|
|
1650
|
+
finishPrompt("No locales selected.");
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
for (const locale of normalized) {
|
|
1654
|
+
await cloneLocaleMessages(cwd, "vi", locale);
|
|
1655
|
+
}
|
|
1656
|
+
const mergedLocales = [
|
|
1657
|
+
.../* @__PURE__ */ new Set([...state.locales, ...normalized])
|
|
1658
|
+
].sort();
|
|
1659
|
+
await patchLocalesConfig(cwd, mergedLocales);
|
|
1660
|
+
await writeManifest(cwd, {
|
|
1661
|
+
...state,
|
|
1662
|
+
locales: mergedLocales
|
|
1663
|
+
});
|
|
1664
|
+
finishPrompt(`Added locales: ${normalized.join(", ")}`);
|
|
1665
|
+
log.info(
|
|
1666
|
+
"Locale files were cloned from Vietnamese values. Translate them when ready."
|
|
1667
|
+
);
|
|
1017
1668
|
});
|
|
1018
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) => {
|
|
1019
1670
|
const cwd = process.cwd();
|
|
1020
|
-
const authFilePath =
|
|
1021
|
-
const hasSrc = await pathExists(
|
|
1022
|
-
const hasPackageJson = await pathExists(
|
|
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"));
|
|
1023
1674
|
const hasAuthFile = await pathExists(authFilePath);
|
|
1024
1675
|
if (!hasSrc || !hasPackageJson || !hasAuthFile) {
|
|
1025
|
-
log.error(
|
|
1676
|
+
log.error(
|
|
1677
|
+
"Run this command from a generated Next.js project with src/lib/auth.ts."
|
|
1678
|
+
);
|
|
1026
1679
|
process.exitCode = 1;
|
|
1027
1680
|
return;
|
|
1028
1681
|
}
|
|
@@ -1039,8 +1692,16 @@ function registerAddCommand(program2) {
|
|
|
1039
1692
|
const selectedProviders = requestedProviders.length > 0 ? [...new Set(requestedProviders)] : options.yes ? [] : await askMultiSelect(
|
|
1040
1693
|
"Select social providers to enable:",
|
|
1041
1694
|
[
|
|
1042
|
-
{
|
|
1043
|
-
|
|
1695
|
+
{
|
|
1696
|
+
value: "google",
|
|
1697
|
+
label: "Google",
|
|
1698
|
+
hint: "Google OAuth login"
|
|
1699
|
+
},
|
|
1700
|
+
{
|
|
1701
|
+
value: "facebook",
|
|
1702
|
+
label: "Facebook",
|
|
1703
|
+
hint: "Facebook OAuth login"
|
|
1704
|
+
}
|
|
1044
1705
|
],
|
|
1045
1706
|
[]
|
|
1046
1707
|
);
|
|
@@ -1048,11 +1709,13 @@ function registerAddCommand(program2) {
|
|
|
1048
1709
|
finishPrompt("No auth providers selected.");
|
|
1049
1710
|
return;
|
|
1050
1711
|
}
|
|
1051
|
-
const authContent = await
|
|
1712
|
+
const authContent = await readFile6(authFilePath, "utf8");
|
|
1052
1713
|
const existingProviders = readConfiguredProviders(authContent);
|
|
1053
|
-
const mergedProviders = [
|
|
1714
|
+
const mergedProviders = [
|
|
1715
|
+
.../* @__PURE__ */ new Set([...existingProviders, ...selectedProviders])
|
|
1716
|
+
];
|
|
1054
1717
|
const nextAuthContent = patchAuthProviders(authContent, mergedProviders);
|
|
1055
|
-
await
|
|
1718
|
+
await writeFile6(authFilePath, nextAuthContent, "utf8");
|
|
1056
1719
|
const envEntries = {};
|
|
1057
1720
|
if (mergedProviders.includes("google")) {
|
|
1058
1721
|
envEntries.GOOGLE_CLIENT_ID = "";
|
|
@@ -1062,19 +1725,17 @@ function registerAddCommand(program2) {
|
|
|
1062
1725
|
envEntries.FACEBOOK_CLIENT_ID = "";
|
|
1063
1726
|
envEntries.FACEBOOK_CLIENT_SECRET = "";
|
|
1064
1727
|
}
|
|
1065
|
-
const envTargets = [
|
|
1066
|
-
".env",
|
|
1067
|
-
".env.example",
|
|
1068
|
-
".env.development"
|
|
1069
|
-
];
|
|
1728
|
+
const envTargets = [".env", ".env.example", ".env.development"];
|
|
1070
1729
|
for (const envFile of envTargets) {
|
|
1071
|
-
const envPath =
|
|
1730
|
+
const envPath = path7.join(cwd, envFile);
|
|
1072
1731
|
if (await pathExists(envPath)) {
|
|
1073
1732
|
await mergeEnvFile(envPath, envEntries);
|
|
1074
1733
|
}
|
|
1075
1734
|
}
|
|
1076
1735
|
finishPrompt(`Enabled providers: ${mergedProviders.join(", ")}`);
|
|
1077
|
-
await ensureBetterAuthGenerate(cwd, {
|
|
1736
|
+
await ensureBetterAuthGenerate(cwd, {
|
|
1737
|
+
nonInteractive: Boolean(options.yes)
|
|
1738
|
+
});
|
|
1078
1739
|
log.step("Next: set provider secrets in .env and restart dev server.");
|
|
1079
1740
|
});
|
|
1080
1741
|
}
|
|
@@ -1082,7 +1743,7 @@ function registerAddCommand(program2) {
|
|
|
1082
1743
|
// src/commands/create.ts
|
|
1083
1744
|
import { spawn as spawn2 } from "child_process";
|
|
1084
1745
|
import { randomBytes } from "crypto";
|
|
1085
|
-
import
|
|
1746
|
+
import path8 from "path";
|
|
1086
1747
|
async function runInstall(packageManager, cwd) {
|
|
1087
1748
|
const installArgsMap = {
|
|
1088
1749
|
npm: ["install"],
|
|
@@ -1138,7 +1799,7 @@ async function resolveProjectName() {
|
|
|
1138
1799
|
}
|
|
1139
1800
|
}
|
|
1140
1801
|
});
|
|
1141
|
-
const targetPath =
|
|
1802
|
+
const targetPath = path8.resolve(process.cwd(), projectName);
|
|
1142
1803
|
if (await pathExists(targetPath)) {
|
|
1143
1804
|
log.error(`Target directory already exists: ${targetPath}`);
|
|
1144
1805
|
continue;
|
|
@@ -1150,8 +1811,8 @@ function registerCreateCommand(program2) {
|
|
|
1150
1811
|
program2.command("create").description("Create a new outsource-ready Next.js app").action(async () => {
|
|
1151
1812
|
startPrompt("NexTCLI project creation");
|
|
1152
1813
|
const projectName = await resolveProjectName();
|
|
1153
|
-
const targetPath =
|
|
1154
|
-
const projectDirectoryName =
|
|
1814
|
+
const targetPath = path8.resolve(process.cwd(), projectName);
|
|
1815
|
+
const projectDirectoryName = path8.basename(targetPath);
|
|
1155
1816
|
const projectSlug = toProjectSlug(projectDirectoryName);
|
|
1156
1817
|
const packageManager = await askSelect(
|
|
1157
1818
|
"Which package manager do you want to use?",
|
|
@@ -1200,7 +1861,7 @@ function registerCreateCommand(program2) {
|
|
|
1200
1861
|
if (Object.keys(moduleEnvEntries).length > 0) {
|
|
1201
1862
|
const envTargets = [".env", ".env.example", ".env.development"];
|
|
1202
1863
|
for (const envFile of envTargets) {
|
|
1203
|
-
const envPath =
|
|
1864
|
+
const envPath = path8.join(targetPath, envFile);
|
|
1204
1865
|
if (await pathExists(envPath)) {
|
|
1205
1866
|
await mergeEnvFile(envPath, moduleEnvEntries);
|
|
1206
1867
|
}
|
|
@@ -1218,7 +1879,7 @@ function registerCreateCommand(program2) {
|
|
|
1218
1879
|
);
|
|
1219
1880
|
if (Object.keys(dependencyEntries).length > 0) {
|
|
1220
1881
|
await addDependencies(
|
|
1221
|
-
|
|
1882
|
+
path8.join(targetPath, "package.json"),
|
|
1222
1883
|
dependencyEntries
|
|
1223
1884
|
);
|
|
1224
1885
|
}
|
|
@@ -1226,8 +1887,22 @@ function registerCreateCommand(program2) {
|
|
|
1226
1887
|
await replaceTokensInDirectory(targetPath, {
|
|
1227
1888
|
__PROJECT_NAME__: projectSlug,
|
|
1228
1889
|
__ENABLE_CHAT__: selectedModules.includes("chat") ? "true" : "false",
|
|
1229
|
-
__BETTER_AUTH_SECRET__: betterAuthSecret
|
|
1890
|
+
__BETTER_AUTH_SECRET__: betterAuthSecret,
|
|
1891
|
+
__NEXTCLI_VERSION__: "0.4.0"
|
|
1230
1892
|
});
|
|
1893
|
+
await mergeModuleSetupSections(
|
|
1894
|
+
targetPath,
|
|
1895
|
+
selectedModules,
|
|
1896
|
+
selectedModules
|
|
1897
|
+
);
|
|
1898
|
+
const manifest = await readManifest(targetPath);
|
|
1899
|
+
if (manifest) {
|
|
1900
|
+
await writeManifest(targetPath, {
|
|
1901
|
+
...manifest,
|
|
1902
|
+
cli: "0.4.0",
|
|
1903
|
+
modules: selectedModules
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1231
1906
|
if (shouldInstall) {
|
|
1232
1907
|
log.step(`Installing dependencies with ${packageManager}...`);
|
|
1233
1908
|
await runInstall(packageManager, targetPath);
|
|
@@ -1249,13 +1924,15 @@ function registerCreateCommand(program2) {
|
|
|
1249
1924
|
"Optional chat schema block was appended to prisma/schema.prisma."
|
|
1250
1925
|
);
|
|
1251
1926
|
}
|
|
1252
|
-
log.step(
|
|
1927
|
+
log.step(
|
|
1928
|
+
`Next: cd ${projectName} && ${packageManager} run db:migrate && ${packageManager} run dev`
|
|
1929
|
+
);
|
|
1253
1930
|
});
|
|
1254
1931
|
}
|
|
1255
1932
|
|
|
1256
1933
|
// src/commands/migrate.ts
|
|
1257
1934
|
import { spawn as spawn3 } from "child_process";
|
|
1258
|
-
import
|
|
1935
|
+
import path9 from "path";
|
|
1259
1936
|
function createDefaultMigrationName() {
|
|
1260
1937
|
const now = /* @__PURE__ */ new Date();
|
|
1261
1938
|
const y = now.getFullYear();
|
|
@@ -1267,16 +1944,16 @@ function createDefaultMigrationName() {
|
|
|
1267
1944
|
return `auto_${y}${m}${d}${hh}${mm}${ss}`;
|
|
1268
1945
|
}
|
|
1269
1946
|
async function detectPackageManager(cwd) {
|
|
1270
|
-
if (await pathExists(
|
|
1947
|
+
if (await pathExists(path9.join(cwd, "bun.lockb"))) {
|
|
1271
1948
|
return "bun";
|
|
1272
1949
|
}
|
|
1273
|
-
if (await pathExists(
|
|
1950
|
+
if (await pathExists(path9.join(cwd, "bun.lock"))) {
|
|
1274
1951
|
return "bun";
|
|
1275
1952
|
}
|
|
1276
|
-
if (await pathExists(
|
|
1953
|
+
if (await pathExists(path9.join(cwd, "pnpm-lock.yaml"))) {
|
|
1277
1954
|
return "pnpm";
|
|
1278
1955
|
}
|
|
1279
|
-
if (await pathExists(
|
|
1956
|
+
if (await pathExists(path9.join(cwd, "yarn.lock"))) {
|
|
1280
1957
|
return "yarn";
|
|
1281
1958
|
}
|
|
1282
1959
|
return "npm";
|
|
@@ -1311,8 +1988,8 @@ async function runCommand2(command, args, cwd) {
|
|
|
1311
1988
|
function registerMigrateCommand(program2) {
|
|
1312
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) => {
|
|
1313
1990
|
const cwd = process.cwd();
|
|
1314
|
-
const hasPackageJson = await pathExists(
|
|
1315
|
-
const hasPrismaSchema = await pathExists(
|
|
1991
|
+
const hasPackageJson = await pathExists(path9.join(cwd, "package.json"));
|
|
1992
|
+
const hasPrismaSchema = await pathExists(path9.join(cwd, "prisma", "schema.prisma"));
|
|
1316
1993
|
if (!hasPackageJson || !hasPrismaSchema) {
|
|
1317
1994
|
log.error(
|
|
1318
1995
|
"Run this command from a generated project root (requires package.json + prisma/schema.prisma)."
|
|
@@ -1464,7 +2141,7 @@ var NexTCLICommand = class _NexTCLICommand extends Command {
|
|
|
1464
2141
|
|
|
1465
2142
|
// src/cli.ts
|
|
1466
2143
|
var program = new NexTCLICommand();
|
|
1467
|
-
program.name("nextcli").description("Scaffold outsource-ready Next.js projects").version("0.
|
|
2144
|
+
program.name("nextcli").description("Scaffold outsource-ready Next.js projects").version("0.4.0");
|
|
1468
2145
|
registerCreateCommand(program);
|
|
1469
2146
|
registerAddCommand(program);
|
|
1470
2147
|
registerMigrateCommand(program);
|