@thinhnguyencth1204/nextcli 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/dist/cli.js +210 -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,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 { readdir as readdir4, 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) {
|
|
@@ -509,7 +554,7 @@ import { readdir as readdir3, readFile as readFile4, writeFile as writeFile4 } f
|
|
|
509
554
|
import path4 from "path";
|
|
510
555
|
import { readdir as readdir2, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
511
556
|
var defaultManifest = {
|
|
512
|
-
cli: "0.
|
|
557
|
+
cli: "0.4.0",
|
|
513
558
|
defaultLocale: "vi",
|
|
514
559
|
locales: ["vi"],
|
|
515
560
|
namespaces: ["common", "auth", "example"],
|
|
@@ -679,6 +724,85 @@ async function writeNamespaceMessages(projectDir, namespace, viTemplate) {
|
|
|
679
724
|
}
|
|
680
725
|
}
|
|
681
726
|
|
|
727
|
+
// src/core/setup-docs.ts
|
|
728
|
+
import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
|
|
729
|
+
import path6 from "path";
|
|
730
|
+
var ENABLED_MODULES_START = "<!-- nextcli:enabled-modules:start -->";
|
|
731
|
+
var ENABLED_MODULES_END = "<!-- nextcli:enabled-modules:end -->";
|
|
732
|
+
var MODULE_ENV_START = "<!-- nextcli:module-env:start -->";
|
|
733
|
+
var MODULE_ENV_END = "<!-- nextcli:module-env:end -->";
|
|
734
|
+
function buildModuleSection(moduleId) {
|
|
735
|
+
const module = getModuleById(moduleId);
|
|
736
|
+
if (!module.setupSection) {
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
return `### Module: ${module.label} (\`${module.id}\`)
|
|
740
|
+
|
|
741
|
+
${module.setupSection.trim()}
|
|
742
|
+
`;
|
|
743
|
+
}
|
|
744
|
+
function formatEnabledModulesLine(moduleIds) {
|
|
745
|
+
if (moduleIds.length === 0) {
|
|
746
|
+
return "**Enabled modules:** none";
|
|
747
|
+
}
|
|
748
|
+
const labels = moduleIds.map((id) => `\`${id}\``).join(", ");
|
|
749
|
+
return `**Enabled modules:** ${labels}`;
|
|
750
|
+
}
|
|
751
|
+
async function updateEnabledModulesLine(content, moduleIds) {
|
|
752
|
+
const startIndex = content.indexOf(ENABLED_MODULES_START);
|
|
753
|
+
const endIndex = content.indexOf(ENABLED_MODULES_END);
|
|
754
|
+
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
|
|
755
|
+
return content;
|
|
756
|
+
}
|
|
757
|
+
const before = content.slice(0, startIndex + ENABLED_MODULES_START.length);
|
|
758
|
+
const after = content.slice(endIndex);
|
|
759
|
+
const line = formatEnabledModulesLine(moduleIds);
|
|
760
|
+
return `${before}
|
|
761
|
+
${line}
|
|
762
|
+
${after}`;
|
|
763
|
+
}
|
|
764
|
+
async function mergeModuleSetupSections(projectDir, moduleIds, allProjectModules) {
|
|
765
|
+
const setupPath = path6.join(projectDir, "SETUP.md");
|
|
766
|
+
if (!await pathExists(setupPath)) {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
let content = await readFile5(setupPath, "utf8");
|
|
770
|
+
const enabledIds = allProjectModules ?? moduleIds;
|
|
771
|
+
content = await updateEnabledModulesLine(content, enabledIds);
|
|
772
|
+
const sections = moduleIds.map((moduleId) => buildModuleSection(moduleId)).filter((section) => Boolean(section));
|
|
773
|
+
if (sections.length === 0) {
|
|
774
|
+
await writeFile5(setupPath, content, "utf8");
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
const startIndex = content.indexOf(MODULE_ENV_START);
|
|
778
|
+
const endIndex = content.indexOf(MODULE_ENV_END);
|
|
779
|
+
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
|
|
780
|
+
await writeFile5(setupPath, content, "utf8");
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
const before = content.slice(0, startIndex + MODULE_ENV_START.length);
|
|
784
|
+
const after = content.slice(endIndex);
|
|
785
|
+
const existingBlock = content.slice(
|
|
786
|
+
startIndex + MODULE_ENV_START.length,
|
|
787
|
+
endIndex
|
|
788
|
+
);
|
|
789
|
+
let nextBlock = existingBlock.trim();
|
|
790
|
+
for (const section of sections) {
|
|
791
|
+
const header = section.split("\n")[0];
|
|
792
|
+
if (header && nextBlock.includes(header)) {
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
nextBlock = nextBlock ? `${nextBlock}
|
|
796
|
+
|
|
797
|
+
${section}` : section;
|
|
798
|
+
}
|
|
799
|
+
content = `${before}
|
|
800
|
+
${nextBlock ? `
|
|
801
|
+
${nextBlock}
|
|
802
|
+
` : "\n"}${after}`;
|
|
803
|
+
await writeFile5(setupPath, content, "utf8");
|
|
804
|
+
}
|
|
805
|
+
|
|
682
806
|
// src/commands/add.ts
|
|
683
807
|
function toKebabCase(input) {
|
|
684
808
|
return input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
@@ -1094,11 +1218,11 @@ export async function DELETE(
|
|
|
1094
1218
|
`;
|
|
1095
1219
|
}
|
|
1096
1220
|
async function appendFeatureModelToPrismaSchema(cwd, modelPascal) {
|
|
1097
|
-
const schemaPath =
|
|
1221
|
+
const schemaPath = path7.join(cwd, "prisma", "schema.prisma");
|
|
1098
1222
|
if (!await pathExists(schemaPath)) {
|
|
1099
1223
|
return "skipped";
|
|
1100
1224
|
}
|
|
1101
|
-
const schemaContent = await
|
|
1225
|
+
const schemaContent = await readFile6(schemaPath, "utf8");
|
|
1102
1226
|
const modelRegex = new RegExp(`\\bmodel\\s+${modelPascal}\\b`);
|
|
1103
1227
|
if (modelRegex.test(schemaContent)) {
|
|
1104
1228
|
return "exists";
|
|
@@ -1115,7 +1239,7 @@ model ${modelPascal} {
|
|
|
1115
1239
|
updatedAt DateTime @updatedAt
|
|
1116
1240
|
}
|
|
1117
1241
|
`;
|
|
1118
|
-
await
|
|
1242
|
+
await writeFile6(
|
|
1119
1243
|
schemaPath,
|
|
1120
1244
|
`${schemaContent.trimEnd()}${modelBlock}
|
|
1121
1245
|
`,
|
|
@@ -1181,18 +1305,18 @@ async function upsertEnvValue(envFilePath, key, value) {
|
|
|
1181
1305
|
if (!await pathExists(envFilePath)) {
|
|
1182
1306
|
return;
|
|
1183
1307
|
}
|
|
1184
|
-
const content = await
|
|
1308
|
+
const content = await readFile6(envFilePath, "utf8");
|
|
1185
1309
|
const entry = `${key}=${value}`;
|
|
1186
1310
|
const pattern = new RegExp(`^${key}=.*$`, "m");
|
|
1187
1311
|
if (pattern.test(content)) {
|
|
1188
1312
|
const next = content.replace(pattern, entry);
|
|
1189
1313
|
if (next !== content) {
|
|
1190
|
-
await
|
|
1314
|
+
await writeFile6(envFilePath, next, "utf8");
|
|
1191
1315
|
}
|
|
1192
1316
|
return;
|
|
1193
1317
|
}
|
|
1194
1318
|
const separator = content.endsWith("\n") || content.length === 0 ? "" : "\n";
|
|
1195
|
-
await
|
|
1319
|
+
await writeFile6(envFilePath, `${content}${separator}${entry}
|
|
1196
1320
|
`, "utf8");
|
|
1197
1321
|
}
|
|
1198
1322
|
function registerAddCommand(program2) {
|
|
@@ -1205,7 +1329,7 @@ function registerAddCommand(program2) {
|
|
|
1205
1329
|
return;
|
|
1206
1330
|
}
|
|
1207
1331
|
const cwd = process.cwd();
|
|
1208
|
-
const srcPath =
|
|
1332
|
+
const srcPath = path7.join(cwd, "src");
|
|
1209
1333
|
if (!await pathExists(srcPath)) {
|
|
1210
1334
|
log.error(
|
|
1211
1335
|
"Run this command from your generated Next.js project root (missing ./src)."
|
|
@@ -1216,36 +1340,36 @@ function registerAddCommand(program2) {
|
|
|
1216
1340
|
const featurePascal = toPascalCase(featureSlug);
|
|
1217
1341
|
const modelPascal = singularizeWord(featurePascal);
|
|
1218
1342
|
const modelDelegate = toCamelCase(modelPascal);
|
|
1219
|
-
const featureRoot =
|
|
1343
|
+
const featureRoot = path7.join(cwd, "src/features", featureSlug);
|
|
1220
1344
|
if (await pathExists(featureRoot)) {
|
|
1221
1345
|
log.error(`Feature already exists: ${featureRoot}`);
|
|
1222
1346
|
process.exitCode = 1;
|
|
1223
1347
|
return;
|
|
1224
1348
|
}
|
|
1225
|
-
await ensureDir(
|
|
1226
|
-
await ensureDir(
|
|
1227
|
-
await
|
|
1228
|
-
|
|
1349
|
+
await ensureDir(path7.join(featureRoot, "api"));
|
|
1350
|
+
await ensureDir(path7.join(featureRoot, "components"));
|
|
1351
|
+
await writeFile6(
|
|
1352
|
+
path7.join(featureRoot, "services.ts"),
|
|
1229
1353
|
buildFeatureServicesContent(modelPascal, modelDelegate),
|
|
1230
1354
|
"utf8"
|
|
1231
1355
|
);
|
|
1232
|
-
await
|
|
1233
|
-
|
|
1356
|
+
await writeFile6(
|
|
1357
|
+
path7.join(featureRoot, "validations.ts"),
|
|
1234
1358
|
buildFeatureValidationContent(modelPascal),
|
|
1235
1359
|
"utf8"
|
|
1236
1360
|
);
|
|
1237
|
-
await
|
|
1238
|
-
|
|
1361
|
+
await writeFile6(
|
|
1362
|
+
path7.join(featureRoot, "api", `use-${featureSlug}.ts`),
|
|
1239
1363
|
buildFeatureHooksContent(featureSlug, modelPascal),
|
|
1240
1364
|
"utf8"
|
|
1241
1365
|
);
|
|
1242
|
-
await
|
|
1243
|
-
|
|
1366
|
+
await writeFile6(
|
|
1367
|
+
path7.join(featureRoot, "components", `${featureSlug}-table.tsx`),
|
|
1244
1368
|
buildFeatureTableContent(featureSlug, modelPascal),
|
|
1245
1369
|
"utf8"
|
|
1246
1370
|
);
|
|
1247
|
-
await
|
|
1248
|
-
|
|
1371
|
+
await writeFile6(
|
|
1372
|
+
path7.join(
|
|
1249
1373
|
featureRoot,
|
|
1250
1374
|
"components",
|
|
1251
1375
|
`create-${featureSlug}-dialog.tsx`
|
|
@@ -1253,39 +1377,39 @@ function registerAddCommand(program2) {
|
|
|
1253
1377
|
buildFeatureDialogContent(featureSlug, modelPascal),
|
|
1254
1378
|
"utf8"
|
|
1255
1379
|
);
|
|
1256
|
-
const routeFilePath =
|
|
1380
|
+
const routeFilePath = path7.join(
|
|
1257
1381
|
cwd,
|
|
1258
1382
|
"src/app/api/v1",
|
|
1259
1383
|
featureSlug,
|
|
1260
1384
|
"route.ts"
|
|
1261
1385
|
);
|
|
1262
|
-
await ensureDir(
|
|
1263
|
-
await
|
|
1386
|
+
await ensureDir(path7.dirname(routeFilePath));
|
|
1387
|
+
await writeFile6(
|
|
1264
1388
|
routeFilePath,
|
|
1265
1389
|
buildCollectionRouteContent(featureSlug, modelPascal),
|
|
1266
1390
|
"utf8"
|
|
1267
1391
|
);
|
|
1268
|
-
const idRoutePath =
|
|
1392
|
+
const idRoutePath = path7.join(
|
|
1269
1393
|
cwd,
|
|
1270
1394
|
"src/app/api/v1",
|
|
1271
1395
|
featureSlug,
|
|
1272
1396
|
"[id]",
|
|
1273
1397
|
"route.ts"
|
|
1274
1398
|
);
|
|
1275
|
-
await ensureDir(
|
|
1276
|
-
await
|
|
1399
|
+
await ensureDir(path7.dirname(idRoutePath));
|
|
1400
|
+
await writeFile6(
|
|
1277
1401
|
idRoutePath,
|
|
1278
1402
|
buildItemRouteContent(featureSlug, modelPascal),
|
|
1279
1403
|
"utf8"
|
|
1280
1404
|
);
|
|
1281
|
-
const featurePagePath =
|
|
1405
|
+
const featurePagePath = path7.join(
|
|
1282
1406
|
cwd,
|
|
1283
1407
|
"src/app/(dashboard)",
|
|
1284
1408
|
featureSlug,
|
|
1285
1409
|
"page.tsx"
|
|
1286
1410
|
);
|
|
1287
|
-
await ensureDir(
|
|
1288
|
-
await
|
|
1411
|
+
await ensureDir(path7.dirname(featurePagePath));
|
|
1412
|
+
await writeFile6(
|
|
1289
1413
|
featurePagePath,
|
|
1290
1414
|
buildFeaturePageContent(featureSlug, modelPascal),
|
|
1291
1415
|
"utf8"
|
|
@@ -1315,8 +1439,8 @@ function registerAddCommand(program2) {
|
|
|
1315
1439
|
});
|
|
1316
1440
|
add.command("module").description("Add optional modules using interactive multi-select").option("--module <module...>", "Preselect module ids").option("--yes", "Skip prompts").action(async (options) => {
|
|
1317
1441
|
const cwd = process.cwd();
|
|
1318
|
-
const hasSrc = await pathExists(
|
|
1319
|
-
const hasPackageJson = await pathExists(
|
|
1442
|
+
const hasSrc = await pathExists(path7.join(cwd, "src"));
|
|
1443
|
+
const hasPackageJson = await pathExists(path7.join(cwd, "package.json"));
|
|
1320
1444
|
if (!hasSrc || !hasPackageJson) {
|
|
1321
1445
|
log.error("Run this command from your generated Next.js project root.");
|
|
1322
1446
|
process.exitCode = 1;
|
|
@@ -1376,7 +1500,7 @@ function registerAddCommand(program2) {
|
|
|
1376
1500
|
if (Object.keys(envEntries).length > 0) {
|
|
1377
1501
|
const envTargets = [".env", ".env.example", ".env.development"];
|
|
1378
1502
|
for (const envFile of envTargets) {
|
|
1379
|
-
const envPath =
|
|
1503
|
+
const envPath = path7.join(cwd, envFile);
|
|
1380
1504
|
if (await pathExists(envPath)) {
|
|
1381
1505
|
await mergeEnvFile(envPath, envEntries);
|
|
1382
1506
|
}
|
|
@@ -1386,7 +1510,7 @@ function registerAddCommand(program2) {
|
|
|
1386
1510
|
const envTargets = [".env", ".env.example", ".env.development"];
|
|
1387
1511
|
for (const envFile of envTargets) {
|
|
1388
1512
|
await upsertEnvValue(
|
|
1389
|
-
|
|
1513
|
+
path7.join(cwd, envFile),
|
|
1390
1514
|
"NEXT_PUBLIC_ENABLE_CHAT",
|
|
1391
1515
|
"true"
|
|
1392
1516
|
);
|
|
@@ -1404,14 +1528,14 @@ function registerAddCommand(program2) {
|
|
|
1404
1528
|
);
|
|
1405
1529
|
if (Object.keys(dependencyEntries).length > 0) {
|
|
1406
1530
|
await addDependencies(
|
|
1407
|
-
|
|
1531
|
+
path7.join(cwd, "package.json"),
|
|
1408
1532
|
dependencyEntries
|
|
1409
1533
|
);
|
|
1410
1534
|
}
|
|
1411
1535
|
const state = await detectProjectState(cwd);
|
|
1412
1536
|
const namespaceSet = new Set(state.namespaces);
|
|
1413
1537
|
for (const moduleId of selectedModules) {
|
|
1414
|
-
const moduleTemplateMessages =
|
|
1538
|
+
const moduleTemplateMessages = path7.join(
|
|
1415
1539
|
getModuleById(moduleId).templatePath,
|
|
1416
1540
|
"messages/vi"
|
|
1417
1541
|
);
|
|
@@ -1428,8 +1552,8 @@ function registerAddCommand(program2) {
|
|
|
1428
1552
|
const namespace = file.name.replace(/\.json$/, "");
|
|
1429
1553
|
namespaceSet.add(namespace);
|
|
1430
1554
|
const templateData = JSON.parse(
|
|
1431
|
-
await
|
|
1432
|
-
|
|
1555
|
+
await readFile6(
|
|
1556
|
+
path7.join(moduleTemplateMessages, file.name),
|
|
1433
1557
|
"utf8"
|
|
1434
1558
|
)
|
|
1435
1559
|
);
|
|
@@ -1442,6 +1566,10 @@ function registerAddCommand(program2) {
|
|
|
1442
1566
|
namespaces: [...namespaceSet],
|
|
1443
1567
|
modules: [.../* @__PURE__ */ new Set([...state.modules, ...selectedModules])]
|
|
1444
1568
|
});
|
|
1569
|
+
const mergedModules = [
|
|
1570
|
+
.../* @__PURE__ */ new Set([...state.modules, ...selectedModules])
|
|
1571
|
+
];
|
|
1572
|
+
await mergeModuleSetupSections(cwd, selectedModules, mergedModules);
|
|
1445
1573
|
finishPrompt(`Added modules: ${selectedModules.join(", ")}`);
|
|
1446
1574
|
log.detail("Copied files", String(copiedFileCount));
|
|
1447
1575
|
if (autoAddedModules.length > 0) {
|
|
@@ -1472,8 +1600,8 @@ function registerAddCommand(program2) {
|
|
|
1472
1600
|
});
|
|
1473
1601
|
add.command("language").description("Add locales and clone message files from Vietnamese").option("--locale <locale...>", "Preselect locales: en,ja,ko").option("--yes", "Skip prompts").action(async (options) => {
|
|
1474
1602
|
const cwd = process.cwd();
|
|
1475
|
-
const hasMessages = await pathExists(
|
|
1476
|
-
const hasConfig = await pathExists(
|
|
1603
|
+
const hasMessages = await pathExists(path7.join(cwd, "messages"));
|
|
1604
|
+
const hasConfig = await pathExists(path7.join(cwd, "src/i18n/config.ts"));
|
|
1477
1605
|
if (!hasMessages || !hasConfig) {
|
|
1478
1606
|
log.error(
|
|
1479
1607
|
"Run this command from a generated Next.js project with i18n scaffold."
|
|
@@ -1540,9 +1668,9 @@ function registerAddCommand(program2) {
|
|
|
1540
1668
|
});
|
|
1541
1669
|
add.command("auth-provider").description("Add social auth providers to existing Better Auth setup").option("--provider <provider...>", "Preselect providers: google,facebook").option("--yes", "Skip prompts").action(async (options) => {
|
|
1542
1670
|
const cwd = process.cwd();
|
|
1543
|
-
const authFilePath =
|
|
1544
|
-
const hasSrc = await pathExists(
|
|
1545
|
-
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"));
|
|
1546
1674
|
const hasAuthFile = await pathExists(authFilePath);
|
|
1547
1675
|
if (!hasSrc || !hasPackageJson || !hasAuthFile) {
|
|
1548
1676
|
log.error(
|
|
@@ -1581,13 +1709,13 @@ function registerAddCommand(program2) {
|
|
|
1581
1709
|
finishPrompt("No auth providers selected.");
|
|
1582
1710
|
return;
|
|
1583
1711
|
}
|
|
1584
|
-
const authContent = await
|
|
1712
|
+
const authContent = await readFile6(authFilePath, "utf8");
|
|
1585
1713
|
const existingProviders = readConfiguredProviders(authContent);
|
|
1586
1714
|
const mergedProviders = [
|
|
1587
1715
|
.../* @__PURE__ */ new Set([...existingProviders, ...selectedProviders])
|
|
1588
1716
|
];
|
|
1589
1717
|
const nextAuthContent = patchAuthProviders(authContent, mergedProviders);
|
|
1590
|
-
await
|
|
1718
|
+
await writeFile6(authFilePath, nextAuthContent, "utf8");
|
|
1591
1719
|
const envEntries = {};
|
|
1592
1720
|
if (mergedProviders.includes("google")) {
|
|
1593
1721
|
envEntries.GOOGLE_CLIENT_ID = "";
|
|
@@ -1599,7 +1727,7 @@ function registerAddCommand(program2) {
|
|
|
1599
1727
|
}
|
|
1600
1728
|
const envTargets = [".env", ".env.example", ".env.development"];
|
|
1601
1729
|
for (const envFile of envTargets) {
|
|
1602
|
-
const envPath =
|
|
1730
|
+
const envPath = path7.join(cwd, envFile);
|
|
1603
1731
|
if (await pathExists(envPath)) {
|
|
1604
1732
|
await mergeEnvFile(envPath, envEntries);
|
|
1605
1733
|
}
|
|
@@ -1615,7 +1743,7 @@ function registerAddCommand(program2) {
|
|
|
1615
1743
|
// src/commands/create.ts
|
|
1616
1744
|
import { spawn as spawn2 } from "child_process";
|
|
1617
1745
|
import { randomBytes } from "crypto";
|
|
1618
|
-
import
|
|
1746
|
+
import path8 from "path";
|
|
1619
1747
|
async function runInstall(packageManager, cwd) {
|
|
1620
1748
|
const installArgsMap = {
|
|
1621
1749
|
npm: ["install"],
|
|
@@ -1671,7 +1799,7 @@ async function resolveProjectName() {
|
|
|
1671
1799
|
}
|
|
1672
1800
|
}
|
|
1673
1801
|
});
|
|
1674
|
-
const targetPath =
|
|
1802
|
+
const targetPath = path8.resolve(process.cwd(), projectName);
|
|
1675
1803
|
if (await pathExists(targetPath)) {
|
|
1676
1804
|
log.error(`Target directory already exists: ${targetPath}`);
|
|
1677
1805
|
continue;
|
|
@@ -1683,8 +1811,8 @@ function registerCreateCommand(program2) {
|
|
|
1683
1811
|
program2.command("create").description("Create a new outsource-ready Next.js app").action(async () => {
|
|
1684
1812
|
startPrompt("NexTCLI project creation");
|
|
1685
1813
|
const projectName = await resolveProjectName();
|
|
1686
|
-
const targetPath =
|
|
1687
|
-
const projectDirectoryName =
|
|
1814
|
+
const targetPath = path8.resolve(process.cwd(), projectName);
|
|
1815
|
+
const projectDirectoryName = path8.basename(targetPath);
|
|
1688
1816
|
const projectSlug = toProjectSlug(projectDirectoryName);
|
|
1689
1817
|
const packageManager = await askSelect(
|
|
1690
1818
|
"Which package manager do you want to use?",
|
|
@@ -1733,7 +1861,7 @@ function registerCreateCommand(program2) {
|
|
|
1733
1861
|
if (Object.keys(moduleEnvEntries).length > 0) {
|
|
1734
1862
|
const envTargets = [".env", ".env.example", ".env.development"];
|
|
1735
1863
|
for (const envFile of envTargets) {
|
|
1736
|
-
const envPath =
|
|
1864
|
+
const envPath = path8.join(targetPath, envFile);
|
|
1737
1865
|
if (await pathExists(envPath)) {
|
|
1738
1866
|
await mergeEnvFile(envPath, moduleEnvEntries);
|
|
1739
1867
|
}
|
|
@@ -1751,7 +1879,7 @@ function registerCreateCommand(program2) {
|
|
|
1751
1879
|
);
|
|
1752
1880
|
if (Object.keys(dependencyEntries).length > 0) {
|
|
1753
1881
|
await addDependencies(
|
|
1754
|
-
|
|
1882
|
+
path8.join(targetPath, "package.json"),
|
|
1755
1883
|
dependencyEntries
|
|
1756
1884
|
);
|
|
1757
1885
|
}
|
|
@@ -1760,13 +1888,18 @@ function registerCreateCommand(program2) {
|
|
|
1760
1888
|
__PROJECT_NAME__: projectSlug,
|
|
1761
1889
|
__ENABLE_CHAT__: selectedModules.includes("chat") ? "true" : "false",
|
|
1762
1890
|
__BETTER_AUTH_SECRET__: betterAuthSecret,
|
|
1763
|
-
__NEXTCLI_VERSION__: "0.
|
|
1891
|
+
__NEXTCLI_VERSION__: "0.4.0"
|
|
1764
1892
|
});
|
|
1893
|
+
await mergeModuleSetupSections(
|
|
1894
|
+
targetPath,
|
|
1895
|
+
selectedModules,
|
|
1896
|
+
selectedModules
|
|
1897
|
+
);
|
|
1765
1898
|
const manifest = await readManifest(targetPath);
|
|
1766
1899
|
if (manifest) {
|
|
1767
1900
|
await writeManifest(targetPath, {
|
|
1768
1901
|
...manifest,
|
|
1769
|
-
cli: "0.
|
|
1902
|
+
cli: "0.4.0",
|
|
1770
1903
|
modules: selectedModules
|
|
1771
1904
|
});
|
|
1772
1905
|
}
|
|
@@ -1791,13 +1924,15 @@ function registerCreateCommand(program2) {
|
|
|
1791
1924
|
"Optional chat schema block was appended to prisma/schema.prisma."
|
|
1792
1925
|
);
|
|
1793
1926
|
}
|
|
1794
|
-
log.step(
|
|
1927
|
+
log.step(
|
|
1928
|
+
`Next: cd ${projectName} && ${packageManager} run db:migrate && ${packageManager} run dev`
|
|
1929
|
+
);
|
|
1795
1930
|
});
|
|
1796
1931
|
}
|
|
1797
1932
|
|
|
1798
1933
|
// src/commands/migrate.ts
|
|
1799
1934
|
import { spawn as spawn3 } from "child_process";
|
|
1800
|
-
import
|
|
1935
|
+
import path9 from "path";
|
|
1801
1936
|
function createDefaultMigrationName() {
|
|
1802
1937
|
const now = /* @__PURE__ */ new Date();
|
|
1803
1938
|
const y = now.getFullYear();
|
|
@@ -1809,16 +1944,16 @@ function createDefaultMigrationName() {
|
|
|
1809
1944
|
return `auto_${y}${m}${d}${hh}${mm}${ss}`;
|
|
1810
1945
|
}
|
|
1811
1946
|
async function detectPackageManager(cwd) {
|
|
1812
|
-
if (await pathExists(
|
|
1947
|
+
if (await pathExists(path9.join(cwd, "bun.lockb"))) {
|
|
1813
1948
|
return "bun";
|
|
1814
1949
|
}
|
|
1815
|
-
if (await pathExists(
|
|
1950
|
+
if (await pathExists(path9.join(cwd, "bun.lock"))) {
|
|
1816
1951
|
return "bun";
|
|
1817
1952
|
}
|
|
1818
|
-
if (await pathExists(
|
|
1953
|
+
if (await pathExists(path9.join(cwd, "pnpm-lock.yaml"))) {
|
|
1819
1954
|
return "pnpm";
|
|
1820
1955
|
}
|
|
1821
|
-
if (await pathExists(
|
|
1956
|
+
if (await pathExists(path9.join(cwd, "yarn.lock"))) {
|
|
1822
1957
|
return "yarn";
|
|
1823
1958
|
}
|
|
1824
1959
|
return "npm";
|
|
@@ -1853,8 +1988,8 @@ async function runCommand2(command, args, cwd) {
|
|
|
1853
1988
|
function registerMigrateCommand(program2) {
|
|
1854
1989
|
program2.command("migrate").description("Run Prisma migration script in current project").option("--name <migration-name>", "Migration name (defaults to auto timestamp)").option("--skip-generate", "Pass --skip-generate to prisma migrate dev").action(async (options) => {
|
|
1855
1990
|
const cwd = process.cwd();
|
|
1856
|
-
const hasPackageJson = await pathExists(
|
|
1857
|
-
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"));
|
|
1858
1993
|
if (!hasPackageJson || !hasPrismaSchema) {
|
|
1859
1994
|
log.error(
|
|
1860
1995
|
"Run this command from a generated project root (requires package.json + prisma/schema.prisma)."
|
|
@@ -2006,7 +2141,7 @@ var NexTCLICommand = class _NexTCLICommand extends Command {
|
|
|
2006
2141
|
|
|
2007
2142
|
// src/cli.ts
|
|
2008
2143
|
var program = new NexTCLICommand();
|
|
2009
|
-
program.name("nextcli").description("Scaffold outsource-ready Next.js projects").version("0.
|
|
2144
|
+
program.name("nextcli").description("Scaffold outsource-ready Next.js projects").version("0.4.0");
|
|
2010
2145
|
registerCreateCommand(program);
|
|
2011
2146
|
registerAddCommand(program);
|
|
2012
2147
|
registerMigrateCommand(program);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thinhnguyencth1204/nextcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "CLI scaffolder for outsourced Next.js projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@clack/prompts": "^0.7.0",
|
|
36
|
+
"@thinhnguyencth1204/nextcli": "^0.4.0",
|
|
36
37
|
"commander": "^12.1.0"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|