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