@thinhnguyencth1204/nextcli 0.6.1 → 0.7.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 +58 -47
- package/dist/cli.js +1002 -753
- package/package.json +4 -2
- package/templates/{next-base/src/lib/axios-instance.ts → features/api/src/lib/api/axios.ts} +7 -2
- package/templates/{next-base/src/lib/api-response.ts → features/api/src/lib/api/response.ts} +1 -5
- package/templates/{next-base → features/auth}/src/app/(auth)/change-password/layout.tsx +1 -1
- package/templates/{next-base → features/auth}/src/app/(auth)/sign-in/layout.tsx +1 -1
- package/templates/{next-base → features/auth}/src/app/api/v1/auth/change-password/route.ts +3 -3
- package/templates/{next-base → features/auth}/src/app/api/v1/auth/login/route.ts +3 -3
- package/templates/{next-base → features/auth}/src/app/api/v1/auth/logout/route.ts +2 -2
- package/templates/{next-base → features/auth}/src/app/api/v1/auth/me/route.ts +2 -2
- package/templates/{next-base → features/auth}/src/app/api/v1/auth/refresh/route.ts +2 -2
- package/templates/{next-base → features/auth}/src/app/api/v1/users/[id]/route.ts +3 -3
- package/templates/{next-base → features/auth}/src/app/api/v1/users/route.ts +3 -3
- package/templates/{next-base → features/auth}/src/features/auth/components/account-panel.tsx +1 -1
- package/templates/{next-base → features/auth}/src/features/auth/components/change-password-form.tsx +1 -1
- package/templates/{next-base → features/auth}/src/features/auth/components/sign-in-form.tsx +2 -2
- package/templates/{next-base → features/auth}/src/features/users/services.ts +1 -1
- package/templates/{next-base → features/auth}/src/instrumentation.ts +1 -1
- package/templates/{next-base/src/lib → features/auth/src/lib/auth}/bootstrap.ts +2 -3
- package/templates/features/auth/src/lib/auth/index.ts +1 -0
- package/templates/{next-base/src/lib → features/auth/src/lib/auth}/rbac.ts +2 -5
- package/templates/{next-base/src/lib/auth.ts → features/auth/src/lib/auth/server.ts} +2 -1
- package/templates/{next-base → features/auth}/src/lib/constants.ts +3 -0
- package/templates/features/chat/src/app/api/v1/chat/route.ts +1 -1
- package/templates/features/chat/src/features/chat/api/use-chat-history.ts +1 -1
- package/templates/features/chat/src/features/chat/api/use-send-message.ts +1 -1
- package/templates/{next-base → features/dashboard}/src/app/(dashboard)/layout.tsx +1 -1
- package/templates/features/dashboard/src/app/page.tsx +5 -0
- package/templates/{next-base → features/dashboard}/src/components/layout/private/nav-user.tsx +1 -1
- package/templates/{next-base → features/database}/prisma/schema.prisma +0 -12
- package/templates/{next-base → features/database}/prisma.config.ts +2 -2
- package/templates/features/database/src/lib/prisma.ts +23 -0
- package/templates/{next-base → features/example}/src/app/api/v1/example/route.ts +2 -2
- package/templates/{next-base → features/example}/src/example/api/use-example.ts +1 -1
- package/templates/{next-base → features/example}/src/example/api/use-mutations.ts +1 -1
- package/templates/{next-base → features/example}/src/example/services.ts +1 -1
- package/templates/features/i18n/next.config.ts +17 -0
- package/templates/features/i18n/src/app/layout.tsx +42 -0
- package/templates/next-base/.env +0 -14
- package/templates/next-base/.env.development +0 -14
- package/templates/next-base/.env.example +0 -14
- package/templates/next-base/PROJECT_STRUCTURE.md +33 -55
- package/templates/next-base/SETUP.md +12 -60
- package/templates/next-base/bun.lock +17 -0
- package/templates/next-base/next.config.ts +1 -4
- package/templates/next-base/nextcli.json +3 -3
- package/templates/next-base/package.json +1 -21
- package/templates/next-base/src/app/layout.tsx +6 -14
- package/templates/next-base/src/app/page.tsx +25 -2
- package/templates/next-base/prisma/migrations/20260612000000_init/migration.sql +0 -104
- package/templates/next-base/prisma/migrations/migration_lock.toml +0 -3
- package/templates/next-base/src/app/(auth)/.gitkeep +0 -1
- /package/templates/{next-base → features/api}/src/components/providers/query-provider.tsx +0 -0
- /package/templates/{next-base/src/lib → features/api/src/lib/api}/token-store.ts +0 -0
- /package/templates/{next-base → features/auth}/messages/vi/auth.json +0 -0
- /package/templates/{next-base/prisma/migrations → features/auth/src/app/(auth)}/.gitkeep +0 -0
- /package/templates/{next-base → features/auth}/src/app/(auth)/change-password/page.tsx +0 -0
- /package/templates/{next-base → features/auth}/src/app/(auth)/layout.tsx +0 -0
- /package/templates/{next-base → features/auth}/src/app/(auth)/sign-in/page.tsx +0 -0
- /package/templates/{next-base → features/auth}/src/app/api/auth/[...all]/route.ts +0 -0
- /package/templates/{next-base → features/auth}/src/features/auth/validations.ts +0 -0
- /package/templates/{next-base → features/auth}/src/features/users/validations.ts +0 -0
- /package/templates/{next-base/src/lib/auth-client.ts → features/auth/src/lib/auth/client.ts} +0 -0
- /package/templates/{next-base/src/lib/auth-cookies.ts → features/auth/src/lib/auth/cookies.ts} +0 -0
- /package/templates/{next-base → features/dashboard}/src/app/(dashboard)/account/page.tsx +0 -0
- /package/templates/{next-base → features/dashboard}/src/app/(dashboard)/dashboard/page.tsx +0 -0
- /package/templates/{next-base → features/dashboard}/src/components/layout/private/app-sidebar.tsx +0 -0
- /package/templates/{next-base → features/dashboard}/src/components/layout/private/dashboard-layout.tsx +0 -0
- /package/templates/{next-base → features/dashboard}/src/components/layout/private/nav-sidebar.tsx +0 -0
- /package/templates/{next-base → features/dashboard}/src/components/ui/data-table/data-table-column-header.tsx +0 -0
- /package/templates/{next-base → features/dashboard}/src/components/ui/data-table/data-table-filter-list.tsx +0 -0
- /package/templates/{next-base → features/dashboard}/src/components/ui/data-table/data-table-pagination.tsx +0 -0
- /package/templates/{next-base → features/dashboard}/src/components/ui/data-table/data-table-skeleton.tsx +0 -0
- /package/templates/{next-base → features/dashboard}/src/components/ui/data-table/data-table-toolbar.tsx +0 -0
- /package/templates/{next-base → features/dashboard}/src/components/ui/data-table/data-table-view-options.tsx +0 -0
- /package/templates/{next-base → features/dashboard}/src/components/ui/data-table/data-table.tsx +0 -0
- /package/templates/{next-base → features/dashboard}/src/components/ui/sidebar.tsx +0 -0
- /package/templates/{next-base → features/dashboard}/src/data/sidebar-modules.ts +0 -0
- /package/templates/{next-base → features/dashboard}/src/hooks/table/use-data-table.ts +0 -0
- /package/templates/{next-base → features/dashboard}/src/hooks/use-mobile.ts +0 -0
- /package/templates/{next-base → features/dashboard}/src/types/data-table.ts +0 -0
- /package/templates/{next-base/src/lib → features/database/src/lib/db}/prisma.ts +0 -0
- /package/templates/{next-base → features/example}/messages/vi/example.json +0 -0
- /package/templates/{next-base → features/example}/src/app/(dashboard)/example/page.tsx +0 -0
- /package/templates/{next-base → features/example}/src/example/components/example-table.tsx +0 -0
- /package/templates/{next-base → features/example}/src/example/validations.ts +0 -0
- /package/templates/{next-base → features/i18n}/messages/vi/common.json +0 -0
- /package/templates/{next-base → features/i18n}/src/components/layout/private/locale-switcher.tsx +0 -0
- /package/templates/{next-base → features/i18n}/src/i18n/config.ts +0 -0
- /package/templates/{next-base → features/i18n}/src/i18n/namespaces.ts +0 -0
- /package/templates/{next-base → features/i18n}/src/i18n/request.ts +0 -0
- /package/templates/{next-base → features/supabase}/src/lib/supabase/client.ts +0 -0
- /package/templates/{next-base → features/supabase}/src/lib/supabase/storage-config.ts +0 -0
- /package/templates/{next-base → features/supabase}/src/lib/supabase/storage.ts +0 -0
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 path10 from "path";
|
|
5
5
|
|
|
6
6
|
// src/core/fs.ts
|
|
7
7
|
import {
|
|
@@ -187,14 +187,27 @@ function formatEnvValue(value) {
|
|
|
187
187
|
}
|
|
188
188
|
return JSON.stringify(value);
|
|
189
189
|
}
|
|
190
|
-
async function
|
|
190
|
+
async function mergePackageJson(packageJsonPath, options) {
|
|
191
191
|
const packageRaw = await readFile(packageJsonPath, "utf8");
|
|
192
192
|
const packageJson = JSON.parse(packageRaw);
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
193
|
+
if (options.dependencies) {
|
|
194
|
+
packageJson.dependencies = {
|
|
195
|
+
...packageJson.dependencies ?? {},
|
|
196
|
+
...options.dependencies
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (options.devDependencies) {
|
|
200
|
+
packageJson.devDependencies = {
|
|
201
|
+
...packageJson.devDependencies ?? {},
|
|
202
|
+
...options.devDependencies
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (options.scripts) {
|
|
206
|
+
packageJson.scripts = {
|
|
207
|
+
...packageJson.scripts ?? {},
|
|
208
|
+
...options.scripts
|
|
209
|
+
};
|
|
210
|
+
}
|
|
198
211
|
await writeFile(
|
|
199
212
|
packageJsonPath,
|
|
200
213
|
`${JSON.stringify(packageJson, null, 2)}
|
|
@@ -203,24 +216,367 @@ async function addDependencies(packageJsonPath, dependencies) {
|
|
|
203
216
|
);
|
|
204
217
|
}
|
|
205
218
|
|
|
206
|
-
// src/
|
|
207
|
-
import {
|
|
219
|
+
// src/core/apply-modules.ts
|
|
220
|
+
import { randomBytes } from "crypto";
|
|
221
|
+
import path6 from "path";
|
|
208
222
|
|
|
209
|
-
// src/core/
|
|
223
|
+
// src/core/chat-schema.ts
|
|
210
224
|
import path2 from "path";
|
|
225
|
+
import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
226
|
+
var chatSchemaBlock = `
|
|
227
|
+
|
|
228
|
+
// ------------------------------------------------------------
|
|
229
|
+
// Optional Chatbox Models (Provider-Agnostic)
|
|
230
|
+
// Review carefully before running migrations on production.
|
|
231
|
+
// ------------------------------------------------------------
|
|
232
|
+
model ChatConversation {
|
|
233
|
+
id String @id @default(cuid())
|
|
234
|
+
title String?
|
|
235
|
+
createdAt DateTime @default(now())
|
|
236
|
+
updatedAt DateTime @updatedAt
|
|
237
|
+
participants ChatParticipant[]
|
|
238
|
+
messages ChatMessage[]
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
model ChatParticipant {
|
|
242
|
+
id String @id @default(cuid())
|
|
243
|
+
conversationId String
|
|
244
|
+
userId String
|
|
245
|
+
role ChatParticipantRole @default(MEMBER)
|
|
246
|
+
createdAt DateTime @default(now())
|
|
247
|
+
conversation ChatConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
|
248
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
249
|
+
|
|
250
|
+
@@unique([conversationId, userId])
|
|
251
|
+
@@index([userId])
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
model ChatMessage {
|
|
255
|
+
id String @id @default(cuid())
|
|
256
|
+
conversationId String
|
|
257
|
+
senderId String
|
|
258
|
+
content String
|
|
259
|
+
kind ChatMessageKind @default(TEXT)
|
|
260
|
+
createdAt DateTime @default(now())
|
|
261
|
+
updatedAt DateTime @updatedAt
|
|
262
|
+
conversation ChatConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
|
263
|
+
sender User @relation(fields: [senderId], references: [id], onDelete: Cascade)
|
|
264
|
+
|
|
265
|
+
@@index([conversationId, createdAt])
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
enum ChatParticipantRole {
|
|
269
|
+
OWNER
|
|
270
|
+
MEMBER
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
enum ChatMessageKind {
|
|
274
|
+
TEXT
|
|
275
|
+
SYSTEM
|
|
276
|
+
}
|
|
277
|
+
`;
|
|
278
|
+
function ensureUserRelations(schema) {
|
|
279
|
+
if (schema.includes("chatParticipants") && schema.includes("chatMessages")) {
|
|
280
|
+
return schema;
|
|
281
|
+
}
|
|
282
|
+
const userBlockRegex = /model User \{[\s\S]*?\n\}/m;
|
|
283
|
+
const userBlock = schema.match(userBlockRegex)?.[0];
|
|
284
|
+
if (!userBlock) {
|
|
285
|
+
return schema;
|
|
286
|
+
}
|
|
287
|
+
const relationLines = [];
|
|
288
|
+
if (!userBlock.includes("chatParticipants")) {
|
|
289
|
+
relationLines.push(" chatParticipants ChatParticipant[]");
|
|
290
|
+
}
|
|
291
|
+
if (!userBlock.includes("chatMessages")) {
|
|
292
|
+
relationLines.push(" chatMessages ChatMessage[]");
|
|
293
|
+
}
|
|
294
|
+
if (relationLines.length === 0) {
|
|
295
|
+
return schema;
|
|
296
|
+
}
|
|
297
|
+
const nextUserBlock = userBlock.replace(/\n\}$/, `
|
|
298
|
+
${relationLines.join("\n")}
|
|
299
|
+
}`);
|
|
300
|
+
return schema.replace(userBlock, nextUserBlock);
|
|
301
|
+
}
|
|
302
|
+
async function ensureChatSchemaInProject(projectRoot) {
|
|
303
|
+
const schemaPath = path2.join(projectRoot, "prisma", "schema.prisma");
|
|
304
|
+
if (!await pathExists(schemaPath)) {
|
|
305
|
+
return "skipped";
|
|
306
|
+
}
|
|
307
|
+
let schema = await readFile2(schemaPath, "utf8");
|
|
308
|
+
schema = ensureUserRelations(schema);
|
|
309
|
+
if (schema.includes("model ChatConversation")) {
|
|
310
|
+
await writeFile2(schemaPath, schema, "utf8");
|
|
311
|
+
return "exists";
|
|
312
|
+
}
|
|
313
|
+
const nextSchema = `${schema.trimEnd()}${chatSchemaBlock}
|
|
314
|
+
`;
|
|
315
|
+
await writeFile2(schemaPath, nextSchema, "utf8");
|
|
316
|
+
return "added";
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/core/example-schema.ts
|
|
320
|
+
import path3 from "path";
|
|
321
|
+
import { readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
322
|
+
var exampleSchemaBlock = `
|
|
323
|
+
|
|
324
|
+
// Starter demo table for template onboarding only.
|
|
325
|
+
// Rename/remove this \`Example\` model before production migration.
|
|
326
|
+
model Example {
|
|
327
|
+
id String @id @default(cuid())
|
|
328
|
+
name String
|
|
329
|
+
description String?
|
|
330
|
+
createdAt DateTime @default(now())
|
|
331
|
+
updatedAt DateTime @updatedAt
|
|
332
|
+
}
|
|
333
|
+
`;
|
|
334
|
+
async function ensureExampleSchemaInProject(projectRoot) {
|
|
335
|
+
const schemaPath = path3.join(projectRoot, "prisma", "schema.prisma");
|
|
336
|
+
if (!await pathExists(schemaPath)) {
|
|
337
|
+
return "skipped";
|
|
338
|
+
}
|
|
339
|
+
const schema = await readFile3(schemaPath, "utf8");
|
|
340
|
+
if (schema.includes("model Example")) {
|
|
341
|
+
return "exists";
|
|
342
|
+
}
|
|
343
|
+
const nextSchema = `${schema.trimEnd()}${exampleSchemaBlock}
|
|
344
|
+
`;
|
|
345
|
+
await writeFile3(schemaPath, nextSchema, "utf8");
|
|
346
|
+
return "added";
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/core/email-module.ts
|
|
350
|
+
var EMAIL_PROVIDER_OPTIONS = ["resend", "smtp"];
|
|
351
|
+
function isEmailProvider(value) {
|
|
352
|
+
return EMAIL_PROVIDER_OPTIONS.includes(value);
|
|
353
|
+
}
|
|
354
|
+
function getEmailProviderEnv(provider) {
|
|
355
|
+
if (provider === "smtp") {
|
|
356
|
+
return {
|
|
357
|
+
EMAIL_PROVIDER: "smtp",
|
|
358
|
+
SMTP_HOST: "",
|
|
359
|
+
SMTP_PORT: "587",
|
|
360
|
+
SMTP_USER: "",
|
|
361
|
+
SMTP_PASSWORD: "",
|
|
362
|
+
SMTP_FROM: ""
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
EMAIL_PROVIDER: "resend",
|
|
367
|
+
RESEND_API_KEY: "",
|
|
368
|
+
RESEND_FROM_EMAIL: ""
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function getEmailProviderDependencies(provider) {
|
|
372
|
+
if (provider === "smtp") {
|
|
373
|
+
return {
|
|
374
|
+
nodemailer: "^6.10.1",
|
|
375
|
+
"@types/nodemailer": "^6.4.17"
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
return {
|
|
379
|
+
resend: "^6.9.2",
|
|
380
|
+
"@react-email/components": "^1.0.12",
|
|
381
|
+
"react-email": "^4.0.0"
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// src/core/module-selection.ts
|
|
386
|
+
var MODULE_ORDER = [
|
|
387
|
+
"database",
|
|
388
|
+
"supabase",
|
|
389
|
+
"auth",
|
|
390
|
+
"api",
|
|
391
|
+
"i18n",
|
|
392
|
+
"dashboard",
|
|
393
|
+
"example",
|
|
394
|
+
"supabase-realtime",
|
|
395
|
+
"chat",
|
|
396
|
+
"seo",
|
|
397
|
+
"email"
|
|
398
|
+
];
|
|
399
|
+
var MODULE_DEPENDENCIES = {
|
|
400
|
+
auth: ["database"],
|
|
401
|
+
i18n: ["api"],
|
|
402
|
+
dashboard: ["auth", "api", "i18n"],
|
|
403
|
+
example: ["dashboard", "database"],
|
|
404
|
+
chat: ["database", "supabase-realtime"],
|
|
405
|
+
"supabase-realtime": ["supabase"]
|
|
406
|
+
};
|
|
407
|
+
function sortModules(moduleIds) {
|
|
408
|
+
const set = new Set(moduleIds);
|
|
409
|
+
return MODULE_ORDER.filter((id) => set.has(id));
|
|
410
|
+
}
|
|
411
|
+
function normalizeModuleSelection(moduleIds) {
|
|
412
|
+
const requested = new Set(moduleIds);
|
|
413
|
+
const autoAdded = [];
|
|
414
|
+
function addDependencies(moduleId) {
|
|
415
|
+
for (const dep of MODULE_DEPENDENCIES[moduleId] ?? []) {
|
|
416
|
+
if (!requested.has(dep)) {
|
|
417
|
+
requested.add(dep);
|
|
418
|
+
autoAdded.push(dep);
|
|
419
|
+
addDependencies(dep);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
for (const moduleId of moduleIds) {
|
|
424
|
+
addDependencies(moduleId);
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
selectedModules: sortModules([...requested]),
|
|
428
|
+
autoAddedModules: autoAdded
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// src/core/templates.ts
|
|
433
|
+
import path4 from "path";
|
|
434
|
+
import { existsSync } from "fs";
|
|
211
435
|
import { fileURLToPath } from "url";
|
|
212
|
-
var currentDir =
|
|
213
|
-
var rootDir =
|
|
436
|
+
var currentDir = path4.dirname(fileURLToPath(import.meta.url));
|
|
437
|
+
var rootDir = [path4.resolve(currentDir, ".."), path4.resolve(currentDir, "../..")].find(
|
|
438
|
+
(dir) => existsSync(path4.join(dir, "templates", "next-base"))
|
|
439
|
+
) ?? path4.resolve(currentDir, "../..");
|
|
214
440
|
var templatePaths = {
|
|
215
|
-
base:
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
441
|
+
base: path4.join(rootDir, "templates/next-base"),
|
|
442
|
+
database: path4.join(rootDir, "templates/features/database"),
|
|
443
|
+
supabase: path4.join(rootDir, "templates/features/supabase"),
|
|
444
|
+
auth: path4.join(rootDir, "templates/features/auth"),
|
|
445
|
+
api: path4.join(rootDir, "templates/features/api"),
|
|
446
|
+
i18n: path4.join(rootDir, "templates/features/i18n"),
|
|
447
|
+
dashboard: path4.join(rootDir, "templates/features/dashboard"),
|
|
448
|
+
example: path4.join(rootDir, "templates/features/example"),
|
|
449
|
+
chat: path4.join(rootDir, "templates/features/chat"),
|
|
450
|
+
supabaseRealtime: path4.join(rootDir, "templates/features/supabase-realtime"),
|
|
451
|
+
seo: path4.join(rootDir, "templates/features/seo"),
|
|
452
|
+
email: path4.join(rootDir, "templates/features/email")
|
|
220
453
|
};
|
|
221
454
|
|
|
222
455
|
// src/core/modules.ts
|
|
223
456
|
var optionalModules = [
|
|
457
|
+
{
|
|
458
|
+
id: "database",
|
|
459
|
+
label: "Database (Prisma + Postgres)",
|
|
460
|
+
description: "Adds Prisma schema, client, migrations scripts, and DATABASE_URL",
|
|
461
|
+
templatePath: templatePaths.database,
|
|
462
|
+
env: {
|
|
463
|
+
DATABASE_URL: "postgresql://postgres.your-project-ref:your-supabase-db-password@aws-0-us-east-1.pooler.supabase.com:6543/postgres?pgbouncer=true",
|
|
464
|
+
DIRECT_URL: "postgresql://postgres.your-project-ref:your-supabase-db-password@aws-0-us-east-1.pooler.supabase.com:5432/postgres"
|
|
465
|
+
},
|
|
466
|
+
dependencies: {
|
|
467
|
+
"@prisma/client": "^7.8.0",
|
|
468
|
+
"@prisma/adapter-pg": "^7.8.0",
|
|
469
|
+
pg: "^8.16.3"
|
|
470
|
+
},
|
|
471
|
+
devDependencies: {
|
|
472
|
+
prisma: "^7.8.0",
|
|
473
|
+
dotenv: "^17.4.2"
|
|
474
|
+
},
|
|
475
|
+
scripts: {
|
|
476
|
+
postinstall: "prisma generate",
|
|
477
|
+
"db:generate": "prisma generate",
|
|
478
|
+
"db:migrate": "prisma migrate dev",
|
|
479
|
+
"db:studio": "prisma studio"
|
|
480
|
+
},
|
|
481
|
+
setupSection: `| Variable | Where to get |
|
|
482
|
+
| -------- | ------------ |
|
|
483
|
+
| \`DATABASE_URL\` | Supabase Dashboard \u2192 Connect \u2192 ORMs \u2192 Prisma \u2192 pooled URL (\`:6543\`, \`?pgbouncer=true\`) |
|
|
484
|
+
| \`DIRECT_URL\` | Supabase Dashboard \u2192 Connect \u2192 direct/session URL (\`:5432\`) |
|
|
485
|
+
|
|
486
|
+
Run \`bun run db:migrate\` after setting \`DATABASE_URL\`.`
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
id: "supabase",
|
|
490
|
+
label: "Supabase client + Storage",
|
|
491
|
+
description: "Adds browser Supabase client and Storage upload helpers",
|
|
492
|
+
templatePath: templatePaths.supabase,
|
|
493
|
+
env: {
|
|
494
|
+
NEXT_PUBLIC_SUPABASE_URL: "",
|
|
495
|
+
NEXT_PUBLIC_SUPABASE_ANON_KEY: "",
|
|
496
|
+
NEXT_PUBLIC_SUPABASE_STORAGE_BUCKET: "public"
|
|
497
|
+
},
|
|
498
|
+
dependencies: {
|
|
499
|
+
"@supabase/supabase-js": "^2.44.2"
|
|
500
|
+
},
|
|
501
|
+
setupSection: `| Variable | Where to get |
|
|
502
|
+
| -------- | ------------ |
|
|
503
|
+
| \`NEXT_PUBLIC_SUPABASE_URL\` | Supabase Dashboard \u2192 Project Settings \u2192 API \u2192 Project URL |
|
|
504
|
+
| \`NEXT_PUBLIC_SUPABASE_ANON_KEY\` | Same page \u2192 \`anon\` \`public\` key |
|
|
505
|
+
| \`NEXT_PUBLIC_SUPABASE_STORAGE_BUCKET\` | Storage bucket name (default scaffold: \`public\`) |
|
|
506
|
+
|
|
507
|
+
Create the bucket and RLS policies in Supabase Dashboard before uploads.`
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
id: "auth",
|
|
511
|
+
label: "Auth (Better Auth + RBAC)",
|
|
512
|
+
description: "Adds Better Auth, sign-in pages, user APIs, bootstrap admin, and JWT refresh",
|
|
513
|
+
templatePath: templatePaths.auth,
|
|
514
|
+
env: {
|
|
515
|
+
BETTER_AUTH_SECRET: "",
|
|
516
|
+
BETTER_AUTH_URL: "http://localhost:3000"
|
|
517
|
+
},
|
|
518
|
+
dependencies: {
|
|
519
|
+
"better-auth": "^1.6.11"
|
|
520
|
+
},
|
|
521
|
+
setupSection: `| Variable | Where to get |
|
|
522
|
+
| -------- | ------------ |
|
|
523
|
+
| \`BETTER_AUTH_SECRET\` | Auto-generated on create; rotate in production |
|
|
524
|
+
| \`BETTER_AUTH_URL\` | Your app URL (e.g. \`http://localhost:3000\`) |
|
|
525
|
+
|
|
526
|
+
Requires \`database\` (auto-added). Bootstrap seeds \`admin\` / \`admin1234\` on first dev start.`
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
id: "api",
|
|
530
|
+
label: "API client (Axios + React Query)",
|
|
531
|
+
description: "Adds unified API envelope helpers and token-aware Axios client",
|
|
532
|
+
templatePath: templatePaths.api,
|
|
533
|
+
env: {},
|
|
534
|
+
dependencies: {
|
|
535
|
+
axios: "^1.7.7",
|
|
536
|
+
"@tanstack/react-query": "^5.56.2",
|
|
537
|
+
"@tanstack/react-query-devtools": "^5.56.2"
|
|
538
|
+
},
|
|
539
|
+
setupSection: `No extra env keys. Use \`publicApi\` / \`protectedApi\` from \`src/lib/api/axios.ts\` and \`ok\` / \`fail\` from \`src/lib/api/response.ts\`.`
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
id: "i18n",
|
|
543
|
+
label: "Internationalization (next-intl)",
|
|
544
|
+
description: "Adds locale config, Vietnamese messages, and locale switcher",
|
|
545
|
+
templatePath: templatePaths.i18n,
|
|
546
|
+
env: {
|
|
547
|
+
NEXT_PUBLIC_DEFAULT_LOCALE: "vi"
|
|
548
|
+
},
|
|
549
|
+
dependencies: {
|
|
550
|
+
"next-intl": "^4.13.0"
|
|
551
|
+
},
|
|
552
|
+
setupSection: `| Variable | Where to get |
|
|
553
|
+
| -------- | ------------ |
|
|
554
|
+
| \`NEXT_PUBLIC_DEFAULT_LOCALE\` | Default locale code (scaffold: \`vi\`) |
|
|
555
|
+
|
|
556
|
+
Add more locales with \`nextcli add language\`.`
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
id: "dashboard",
|
|
560
|
+
label: "Dashboard shell",
|
|
561
|
+
description: "Adds protected dashboard layout, sidebar navigation, and data-table UI",
|
|
562
|
+
templatePath: templatePaths.dashboard,
|
|
563
|
+
env: {},
|
|
564
|
+
dependencies: {
|
|
565
|
+
"@tanstack/react-table": "^8.20.5",
|
|
566
|
+
"@dnd-kit/core": "^6.3.1",
|
|
567
|
+
nuqs: "^2.8.1",
|
|
568
|
+
"date-fns": "^3.6.0"
|
|
569
|
+
},
|
|
570
|
+
setupSection: `Requires \`auth\`, \`api\`, and \`i18n\` (auto-added). Protected routes redirect unauthenticated users to \`/sign-in\`.`
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
id: "example",
|
|
574
|
+
label: "Example CRUD demo",
|
|
575
|
+
description: "Adds starter example feature with API route, hooks, and Prisma model",
|
|
576
|
+
templatePath: templatePaths.example,
|
|
577
|
+
env: {},
|
|
578
|
+
setupSection: `Requires \`dashboard\` and \`database\` (auto-added). Appends \`Example\` model to \`prisma/schema.prisma\` \u2014 run \`db:migrate\` after add.`
|
|
579
|
+
},
|
|
224
580
|
{
|
|
225
581
|
id: "chat",
|
|
226
582
|
label: "Chat module",
|
|
@@ -233,21 +589,16 @@ var optionalModules = [
|
|
|
233
589
|
| -------- | ------------ |
|
|
234
590
|
| \`NEXT_PUBLIC_ENABLE_CHAT\` | Set \`true\` when chat module is enabled (auto on add) |
|
|
235
591
|
|
|
236
|
-
Requires \`supabase-realtime\` (auto-added). Run \`db:migrate\` after add \u2014 chat Prisma models are appended.`
|
|
592
|
+
Requires \`database\`, \`supabase\`, and \`supabase-realtime\` (auto-added). Run \`db:migrate\` after add \u2014 chat Prisma models are appended.`
|
|
237
593
|
},
|
|
238
594
|
{
|
|
239
595
|
id: "supabase-realtime",
|
|
240
596
|
label: "Supabase Realtime",
|
|
241
597
|
description: "Adds Realtime channel helper and hooks",
|
|
242
598
|
templatePath: templatePaths.supabaseRealtime,
|
|
243
|
-
env: {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
},
|
|
247
|
-
dependencies: {
|
|
248
|
-
"@supabase/supabase-js": "^2.44.2"
|
|
249
|
-
},
|
|
250
|
-
setupSection: `Uses the base Supabase URL/anon key. Enable Realtime on tables in Supabase Dashboard \u2192 Database \u2192 Replication.`
|
|
599
|
+
env: {},
|
|
600
|
+
dependencies: {},
|
|
601
|
+
setupSection: `Requires \`supabase\` (auto-added). Enable Realtime on tables in Supabase Dashboard \u2192 Database \u2192 Replication.`
|
|
251
602
|
},
|
|
252
603
|
{
|
|
253
604
|
id: "seo",
|
|
@@ -279,330 +630,208 @@ function getModuleById(moduleId) {
|
|
|
279
630
|
return module;
|
|
280
631
|
}
|
|
281
632
|
|
|
282
|
-
// src/core/
|
|
283
|
-
import {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
// src/core/theme.ts
|
|
295
|
-
var reset = "\x1B[0m";
|
|
296
|
-
function paint(code, text2) {
|
|
297
|
-
return `${code}${text2}${reset}`;
|
|
298
|
-
}
|
|
299
|
-
var theme = {
|
|
300
|
-
cyan: (text2) => paint("\x1B[36m", text2),
|
|
301
|
-
green: (text2) => paint("\x1B[32m", text2),
|
|
302
|
-
yellow: (text2) => paint("\x1B[33m", text2),
|
|
303
|
-
red: (text2) => paint("\x1B[31m", text2),
|
|
304
|
-
blue: (text2) => paint("\x1B[34m", text2),
|
|
305
|
-
magenta: (text2) => paint("\x1B[35m", text2),
|
|
306
|
-
dim: (text2) => paint("\x1B[2m", text2),
|
|
307
|
-
bold: (text2) => paint("\x1B[1m", text2)
|
|
308
|
-
};
|
|
309
|
-
var bannerGradient = [
|
|
310
|
-
"\x1B[38;5;51m",
|
|
311
|
-
"\x1B[38;5;45m",
|
|
312
|
-
"\x1B[38;5;39m",
|
|
313
|
-
"\x1B[38;5;33m",
|
|
314
|
-
"\x1B[38;5;27m"
|
|
315
|
-
];
|
|
316
|
-
var log = {
|
|
317
|
-
info(message) {
|
|
318
|
-
console.log(`${theme.cyan("\u2139")} ${message}`);
|
|
319
|
-
},
|
|
320
|
-
success(message) {
|
|
321
|
-
console.log(`${theme.green("\u2714")} ${message}`);
|
|
322
|
-
},
|
|
323
|
-
warn(message) {
|
|
324
|
-
console.log(`${theme.yellow("\u26A0")} ${message}`);
|
|
325
|
-
},
|
|
326
|
-
error(message) {
|
|
327
|
-
console.error(`${theme.red("\u2716")} ${message}`);
|
|
328
|
-
},
|
|
329
|
-
step(message) {
|
|
330
|
-
console.log(`${theme.blue("\u2192")} ${message}`);
|
|
331
|
-
},
|
|
332
|
-
detail(label, value) {
|
|
333
|
-
console.log(`${theme.dim(`${label}:`)} ${theme.cyan(value)}`);
|
|
334
|
-
},
|
|
335
|
-
command(command) {
|
|
336
|
-
console.log(`${theme.dim(" ")}${theme.magenta(command)}`);
|
|
633
|
+
// src/core/setup-docs.ts
|
|
634
|
+
import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
635
|
+
import path5 from "path";
|
|
636
|
+
var ENABLED_MODULES_START = "<!-- nextcli:enabled-modules:start -->";
|
|
637
|
+
var ENABLED_MODULES_END = "<!-- nextcli:enabled-modules:end -->";
|
|
638
|
+
var MODULE_ENV_START = "<!-- nextcli:module-env:start -->";
|
|
639
|
+
var MODULE_ENV_END = "<!-- nextcli:module-env:end -->";
|
|
640
|
+
function buildModuleSection(moduleId) {
|
|
641
|
+
const module = getModuleById(moduleId);
|
|
642
|
+
if (!module.setupSection) {
|
|
643
|
+
return null;
|
|
337
644
|
}
|
|
338
|
-
}
|
|
645
|
+
return `### Module: ${module.label} (\`${module.id}\`)
|
|
339
646
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (!isCancel(value)) {
|
|
343
|
-
return;
|
|
344
|
-
}
|
|
345
|
-
cancel(theme.red("Operation cancelled."));
|
|
346
|
-
process.exit(0);
|
|
347
|
-
}
|
|
348
|
-
function startPrompt(title) {
|
|
349
|
-
intro(theme.cyan(theme.bold(title)));
|
|
350
|
-
}
|
|
351
|
-
function finishPrompt(message) {
|
|
352
|
-
outro(theme.green(message));
|
|
353
|
-
}
|
|
354
|
-
async function askSelect(message, options, initialValue) {
|
|
355
|
-
const value = await select({
|
|
356
|
-
message,
|
|
357
|
-
options,
|
|
358
|
-
initialValue
|
|
359
|
-
});
|
|
360
|
-
handleCancelled(value);
|
|
361
|
-
return value;
|
|
362
|
-
}
|
|
363
|
-
async function askMultiSelect(message, options, initialValues = []) {
|
|
364
|
-
const values = await multiselect({
|
|
365
|
-
message,
|
|
366
|
-
options,
|
|
367
|
-
initialValues,
|
|
368
|
-
required: false
|
|
369
|
-
});
|
|
370
|
-
handleCancelled(values);
|
|
371
|
-
return values;
|
|
372
|
-
}
|
|
373
|
-
async function askConfirm(message, initialValue = false) {
|
|
374
|
-
const value = await confirm({
|
|
375
|
-
message,
|
|
376
|
-
initialValue
|
|
377
|
-
});
|
|
378
|
-
handleCancelled(value);
|
|
379
|
-
return Boolean(value);
|
|
380
|
-
}
|
|
381
|
-
async function askText(message, options = {}) {
|
|
382
|
-
const value = await text({
|
|
383
|
-
message,
|
|
384
|
-
placeholder: options.placeholder,
|
|
385
|
-
initialValue: options.initialValue,
|
|
386
|
-
validate: options.validate
|
|
387
|
-
});
|
|
388
|
-
handleCancelled(value);
|
|
389
|
-
return value;
|
|
647
|
+
${module.setupSection.trim()}
|
|
648
|
+
`;
|
|
390
649
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
const child = spawn(command, args, {
|
|
398
|
-
cwd,
|
|
399
|
-
shell: process.platform === "win32",
|
|
400
|
-
stdio: silent ? "pipe" : "inherit"
|
|
401
|
-
});
|
|
402
|
-
if (silent) {
|
|
403
|
-
child.stdout?.on("data", (chunk) => chunks.push(String(chunk)));
|
|
404
|
-
child.stderr?.on("data", (chunk) => chunks.push(String(chunk)));
|
|
405
|
-
}
|
|
406
|
-
child.on("error", () => {
|
|
407
|
-
resolve({ ok: false, output: chunks.join("") });
|
|
408
|
-
});
|
|
409
|
-
child.on("close", (code) => {
|
|
410
|
-
resolve({ ok: code === 0, output: chunks.join("") });
|
|
411
|
-
});
|
|
412
|
-
});
|
|
650
|
+
function formatEnabledModulesLine(moduleIds) {
|
|
651
|
+
if (moduleIds.length === 0) {
|
|
652
|
+
return "**Enabled modules:** none";
|
|
653
|
+
}
|
|
654
|
+
const labels = moduleIds.map((id) => `\`${id}\``).join(", ");
|
|
655
|
+
return `**Enabled modules:** ${labels}`;
|
|
413
656
|
}
|
|
414
|
-
async function
|
|
415
|
-
const
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
if (options.nonInteractive) {
|
|
420
|
-
log.warn("Better Auth CLI not detected. Skipping schema generation in non-interactive mode.");
|
|
421
|
-
return;
|
|
422
|
-
}
|
|
423
|
-
const approved = await askConfirm(
|
|
424
|
-
"Better Auth CLI is not installed. Install/use npx auth@latest and run schema generation now?",
|
|
425
|
-
true
|
|
426
|
-
);
|
|
427
|
-
if (!approved) {
|
|
428
|
-
log.warn("Skipped Better Auth schema generation.");
|
|
429
|
-
log.command("npx auth@latest generate --config src/lib/auth.ts");
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
canInstall = true;
|
|
657
|
+
async function updateEnabledModulesLine(content, moduleIds) {
|
|
658
|
+
const startIndex = content.indexOf(ENABLED_MODULES_START);
|
|
659
|
+
const endIndex = content.indexOf(ENABLED_MODULES_END);
|
|
660
|
+
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
|
|
661
|
+
return content;
|
|
433
662
|
}
|
|
434
|
-
|
|
663
|
+
const before = content.slice(0, startIndex + ENABLED_MODULES_START.length);
|
|
664
|
+
const after = content.slice(endIndex);
|
|
665
|
+
const line = formatEnabledModulesLine(moduleIds);
|
|
666
|
+
return `${before}
|
|
667
|
+
${line}
|
|
668
|
+
${after}`;
|
|
669
|
+
}
|
|
670
|
+
async function mergeModuleSetupSections(projectDir, moduleIds, allProjectModules) {
|
|
671
|
+
const setupPath = path5.join(projectDir, "SETUP.md");
|
|
672
|
+
if (!await pathExists(setupPath)) {
|
|
435
673
|
return;
|
|
436
674
|
}
|
|
437
|
-
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
// src/core/email-module.ts
|
|
446
|
-
var EMAIL_PROVIDER_OPTIONS = ["resend", "smtp"];
|
|
447
|
-
function isEmailProvider(value) {
|
|
448
|
-
return EMAIL_PROVIDER_OPTIONS.includes(value);
|
|
449
|
-
}
|
|
450
|
-
function getEmailProviderEnv(provider) {
|
|
451
|
-
if (provider === "smtp") {
|
|
452
|
-
return {
|
|
453
|
-
EMAIL_PROVIDER: "smtp",
|
|
454
|
-
SMTP_HOST: "",
|
|
455
|
-
SMTP_PORT: "587",
|
|
456
|
-
SMTP_USER: "",
|
|
457
|
-
SMTP_PASSWORD: "",
|
|
458
|
-
SMTP_FROM: ""
|
|
459
|
-
};
|
|
675
|
+
let content = await readFile4(setupPath, "utf8");
|
|
676
|
+
const enabledIds = allProjectModules ?? moduleIds;
|
|
677
|
+
content = await updateEnabledModulesLine(content, enabledIds);
|
|
678
|
+
const sections = moduleIds.map((moduleId) => buildModuleSection(moduleId)).filter((section) => Boolean(section));
|
|
679
|
+
if (sections.length === 0) {
|
|
680
|
+
await writeFile4(setupPath, content, "utf8");
|
|
681
|
+
return;
|
|
460
682
|
}
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
}
|
|
467
|
-
function getEmailProviderDependencies(provider) {
|
|
468
|
-
if (provider === "smtp") {
|
|
469
|
-
return {
|
|
470
|
-
nodemailer: "^6.10.1",
|
|
471
|
-
"@types/nodemailer": "^6.4.17"
|
|
472
|
-
};
|
|
683
|
+
const startIndex = content.indexOf(MODULE_ENV_START);
|
|
684
|
+
const endIndex = content.indexOf(MODULE_ENV_END);
|
|
685
|
+
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
|
|
686
|
+
await writeFile4(setupPath, content, "utf8");
|
|
687
|
+
return;
|
|
473
688
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
// Optional Chatbox Models (Provider-Agnostic)
|
|
488
|
-
// Review carefully before running migrations on production.
|
|
489
|
-
// ------------------------------------------------------------
|
|
490
|
-
model ChatConversation {
|
|
491
|
-
id String @id @default(cuid())
|
|
492
|
-
title String?
|
|
493
|
-
createdAt DateTime @default(now())
|
|
494
|
-
updatedAt DateTime @updatedAt
|
|
495
|
-
participants ChatParticipant[]
|
|
496
|
-
messages ChatMessage[]
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
model ChatParticipant {
|
|
500
|
-
id String @id @default(cuid())
|
|
501
|
-
conversationId String
|
|
502
|
-
userId String
|
|
503
|
-
role ChatParticipantRole @default(MEMBER)
|
|
504
|
-
createdAt DateTime @default(now())
|
|
505
|
-
conversation ChatConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
|
506
|
-
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
507
|
-
|
|
508
|
-
@@unique([conversationId, userId])
|
|
509
|
-
@@index([userId])
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
model ChatMessage {
|
|
513
|
-
id String @id @default(cuid())
|
|
514
|
-
conversationId String
|
|
515
|
-
senderId String
|
|
516
|
-
content String
|
|
517
|
-
kind ChatMessageKind @default(TEXT)
|
|
518
|
-
createdAt DateTime @default(now())
|
|
519
|
-
updatedAt DateTime @updatedAt
|
|
520
|
-
conversation ChatConversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
|
521
|
-
sender User @relation(fields: [senderId], references: [id], onDelete: Cascade)
|
|
522
|
-
|
|
523
|
-
@@index([conversationId, createdAt])
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
enum ChatParticipantRole {
|
|
527
|
-
OWNER
|
|
528
|
-
MEMBER
|
|
529
|
-
}
|
|
689
|
+
const before = content.slice(0, startIndex + MODULE_ENV_START.length);
|
|
690
|
+
const after = content.slice(endIndex);
|
|
691
|
+
const existingBlock = content.slice(
|
|
692
|
+
startIndex + MODULE_ENV_START.length,
|
|
693
|
+
endIndex
|
|
694
|
+
);
|
|
695
|
+
let nextBlock = existingBlock.trim();
|
|
696
|
+
for (const section of sections) {
|
|
697
|
+
const header = section.split("\n")[0];
|
|
698
|
+
if (header && nextBlock.includes(header)) {
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
nextBlock = nextBlock ? `${nextBlock}
|
|
530
702
|
|
|
531
|
-
|
|
532
|
-
TEXT
|
|
533
|
-
SYSTEM
|
|
534
|
-
}
|
|
535
|
-
`;
|
|
536
|
-
function ensureUserRelations(schema) {
|
|
537
|
-
if (schema.includes("chatParticipants") && schema.includes("chatMessages")) {
|
|
538
|
-
return schema;
|
|
703
|
+
${section}` : section;
|
|
539
704
|
}
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
705
|
+
content = `${before}
|
|
706
|
+
${nextBlock ? `
|
|
707
|
+
${nextBlock}
|
|
708
|
+
` : "\n"}${after}`;
|
|
709
|
+
await writeFile4(setupPath, content, "utf8");
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// src/core/apply-modules.ts
|
|
713
|
+
function resolveModuleEnv(moduleId, emailProvider, authSecret) {
|
|
714
|
+
if (moduleId === "email" && emailProvider) {
|
|
715
|
+
return getEmailProviderEnv(emailProvider);
|
|
544
716
|
}
|
|
545
|
-
const
|
|
546
|
-
|
|
547
|
-
|
|
717
|
+
const module = getModuleById(moduleId);
|
|
718
|
+
const env = { ...module.env };
|
|
719
|
+
if (moduleId === "auth" && authSecret) {
|
|
720
|
+
env.BETTER_AUTH_SECRET = authSecret;
|
|
721
|
+
}
|
|
722
|
+
return env;
|
|
723
|
+
}
|
|
724
|
+
async function applyModulesToProject(options) {
|
|
725
|
+
const { projectDir, moduleIds, emailProvider, safeCopy = false } = options;
|
|
726
|
+
const authSecret = options.authSecret ?? randomBytes(32).toString("base64url");
|
|
727
|
+
const { selectedModules, autoAddedModules } = normalizeModuleSelection(moduleIds);
|
|
728
|
+
const copyReports = [];
|
|
729
|
+
for (const moduleId of selectedModules) {
|
|
730
|
+
const module = getModuleById(moduleId);
|
|
731
|
+
if (safeCopy) {
|
|
732
|
+
copyReports.push({
|
|
733
|
+
moduleId,
|
|
734
|
+
report: await copyDirectorySafely(module.templatePath, projectDir)
|
|
735
|
+
});
|
|
736
|
+
} else {
|
|
737
|
+
await copyDirectory(module.templatePath, projectDir);
|
|
738
|
+
}
|
|
548
739
|
}
|
|
549
|
-
|
|
550
|
-
|
|
740
|
+
let chatSchemaStatus;
|
|
741
|
+
if (selectedModules.includes("chat")) {
|
|
742
|
+
chatSchemaStatus = await ensureChatSchemaInProject(projectDir);
|
|
551
743
|
}
|
|
552
|
-
|
|
553
|
-
|
|
744
|
+
let exampleSchemaStatus;
|
|
745
|
+
if (selectedModules.includes("example")) {
|
|
746
|
+
exampleSchemaStatus = await ensureExampleSchemaInProject(projectDir);
|
|
554
747
|
}
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
748
|
+
const envTargets = [".env", ".env.example", ".env.development"];
|
|
749
|
+
for (const moduleId of selectedModules) {
|
|
750
|
+
const moduleEnv = resolveModuleEnv(moduleId, emailProvider, authSecret);
|
|
751
|
+
if (Object.keys(moduleEnv).length === 0) {
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
for (const envFile of envTargets) {
|
|
755
|
+
const envPath = path6.join(projectDir, envFile);
|
|
756
|
+
if (await pathExists(envPath)) {
|
|
757
|
+
await mergeEnvFile(envPath, moduleEnv, {
|
|
758
|
+
header: `# --- module: ${moduleId} ---`
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
}
|
|
564
762
|
}
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
763
|
+
const packageJsonPath = path6.join(projectDir, "package.json");
|
|
764
|
+
if (await pathExists(packageJsonPath)) {
|
|
765
|
+
const dependencies = {};
|
|
766
|
+
const devDependencies = {};
|
|
767
|
+
const scripts = {};
|
|
768
|
+
for (const moduleId of selectedModules) {
|
|
769
|
+
const module = getModuleById(moduleId);
|
|
770
|
+
Object.assign(dependencies, module.dependencies ?? {});
|
|
771
|
+
Object.assign(devDependencies, module.devDependencies ?? {});
|
|
772
|
+
Object.assign(scripts, module.scripts ?? {});
|
|
773
|
+
if (moduleId === "email" && emailProvider) {
|
|
774
|
+
Object.assign(
|
|
775
|
+
dependencies,
|
|
776
|
+
getEmailProviderDependencies(emailProvider)
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
await mergePackageJson(packageJsonPath, {
|
|
781
|
+
dependencies,
|
|
782
|
+
devDependencies,
|
|
783
|
+
scripts
|
|
784
|
+
});
|
|
570
785
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
786
|
+
return {
|
|
787
|
+
selectedModules,
|
|
788
|
+
autoAddedModules,
|
|
789
|
+
copyReports,
|
|
790
|
+
chatSchemaStatus,
|
|
791
|
+
exampleSchemaStatus
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
async function mergeModuleDocs(projectDir, selectedModules, allProjectModules) {
|
|
795
|
+
await mergeModuleSetupSections(
|
|
796
|
+
projectDir,
|
|
797
|
+
selectedModules,
|
|
798
|
+
allProjectModules
|
|
799
|
+
);
|
|
575
800
|
}
|
|
576
801
|
|
|
802
|
+
// src/core/add-feature.ts
|
|
803
|
+
import path9 from "path";
|
|
804
|
+
import { readFile as readFile7, writeFile as writeFile7 } from "fs/promises";
|
|
805
|
+
|
|
577
806
|
// src/core/i18n.ts
|
|
578
|
-
import
|
|
579
|
-
import { readdir as readdir3, readFile as
|
|
807
|
+
import path8 from "path";
|
|
808
|
+
import { readdir as readdir3, readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
|
|
580
809
|
|
|
581
810
|
// src/core/manifest.ts
|
|
582
|
-
import
|
|
583
|
-
import { readdir as readdir2, readFile as
|
|
811
|
+
import path7 from "path";
|
|
812
|
+
import { readdir as readdir2, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
|
|
584
813
|
var defaultManifest = {
|
|
585
|
-
cli: "0.
|
|
814
|
+
cli: "0.7.0",
|
|
586
815
|
defaultLocale: "vi",
|
|
587
|
-
locales: [
|
|
588
|
-
namespaces: [
|
|
816
|
+
locales: [],
|
|
817
|
+
namespaces: [],
|
|
589
818
|
modules: [],
|
|
590
|
-
features: [
|
|
819
|
+
features: []
|
|
591
820
|
};
|
|
592
821
|
function getManifestPath(projectDir) {
|
|
593
|
-
return
|
|
822
|
+
return path7.join(projectDir, "nextcli.json");
|
|
594
823
|
}
|
|
595
824
|
async function readManifest(projectDir) {
|
|
596
825
|
const manifestPath = getManifestPath(projectDir);
|
|
597
826
|
if (!await pathExists(manifestPath)) {
|
|
598
827
|
return null;
|
|
599
828
|
}
|
|
600
|
-
const raw = await
|
|
829
|
+
const raw = await readFile5(manifestPath, "utf8");
|
|
601
830
|
return JSON.parse(raw);
|
|
602
831
|
}
|
|
603
832
|
async function writeManifest(projectDir, manifest) {
|
|
604
833
|
const manifestPath = getManifestPath(projectDir);
|
|
605
|
-
await
|
|
834
|
+
await writeFile5(
|
|
606
835
|
manifestPath,
|
|
607
836
|
`${JSON.stringify(manifest, null, 2)}
|
|
608
837
|
`,
|
|
@@ -618,22 +847,22 @@ function parseConstArray(content, marker) {
|
|
|
618
847
|
return match[1].split(",").map((item) => item.trim().replaceAll('"', "").replaceAll("'", "")).filter(Boolean);
|
|
619
848
|
}
|
|
620
849
|
async function detectLocalesFromDisk(projectDir) {
|
|
621
|
-
const messagesDir =
|
|
850
|
+
const messagesDir = path7.join(projectDir, "messages");
|
|
622
851
|
if (!await pathExists(messagesDir)) {
|
|
623
|
-
return [
|
|
852
|
+
return [];
|
|
624
853
|
}
|
|
625
854
|
const entries = await readdir2(messagesDir, { withFileTypes: true });
|
|
626
855
|
const locales = entries.filter((item) => item.isDirectory()).map((item) => item.name);
|
|
627
|
-
return locales.
|
|
856
|
+
return locales.sort();
|
|
628
857
|
}
|
|
629
858
|
async function detectNamespacesFromDisk(projectDir) {
|
|
630
|
-
const namespaceFile =
|
|
859
|
+
const namespaceFile = path7.join(projectDir, "src/i18n/namespaces.ts");
|
|
631
860
|
if (!await pathExists(namespaceFile)) {
|
|
632
|
-
return [
|
|
861
|
+
return [];
|
|
633
862
|
}
|
|
634
|
-
const content = await
|
|
863
|
+
const content = await readFile5(namespaceFile, "utf8");
|
|
635
864
|
const namespaces = parseConstArray(content, "namespaces");
|
|
636
|
-
return namespaces
|
|
865
|
+
return namespaces;
|
|
637
866
|
}
|
|
638
867
|
async function reconcileManifest(projectDir) {
|
|
639
868
|
const localesFromDisk = await detectLocalesFromDisk(projectDir);
|
|
@@ -647,7 +876,9 @@ async function reconcileManifest(projectDir) {
|
|
|
647
876
|
]
|
|
648
877
|
};
|
|
649
878
|
merged.defaultLocale = merged.locales.includes(merged.defaultLocale) ? merged.defaultLocale : merged.locales[0] ?? "vi";
|
|
650
|
-
merged.modules = [...new Set(merged.modules)].map(
|
|
879
|
+
merged.modules = [...new Set(merged.modules)].map(
|
|
880
|
+
(moduleId) => moduleId === "resend" ? "email" : moduleId
|
|
881
|
+
);
|
|
651
882
|
merged.features = [...new Set(merged.features)];
|
|
652
883
|
await writeManifest(projectDir, merged);
|
|
653
884
|
return merged;
|
|
@@ -674,34 +905,34 @@ async function detectProjectState(projectDir) {
|
|
|
674
905
|
return reconcileManifest(projectDir);
|
|
675
906
|
}
|
|
676
907
|
async function patchLocalesConfig(projectDir, locales) {
|
|
677
|
-
const configPath =
|
|
908
|
+
const configPath = path8.join(projectDir, "src/i18n/config.ts");
|
|
678
909
|
if (!await pathExists(configPath)) {
|
|
679
910
|
return;
|
|
680
911
|
}
|
|
681
|
-
const content = await
|
|
912
|
+
const content = await readFile6(configPath, "utf8");
|
|
682
913
|
const next = patchBetweenMarkers(
|
|
683
914
|
content,
|
|
684
915
|
localeStartMarker,
|
|
685
916
|
localeEndMarker,
|
|
686
917
|
`export const locales = [${formatArray(locales)}] as const;`
|
|
687
918
|
);
|
|
688
|
-
await
|
|
919
|
+
await writeFile6(configPath, next, "utf8");
|
|
689
920
|
}
|
|
690
921
|
async function appendNamespace(projectDir, namespace) {
|
|
691
|
-
const namespacePath =
|
|
922
|
+
const namespacePath = path8.join(projectDir, "src/i18n/namespaces.ts");
|
|
692
923
|
if (!await pathExists(namespacePath)) {
|
|
693
924
|
return [];
|
|
694
925
|
}
|
|
695
926
|
const currentState = await detectProjectState(projectDir);
|
|
696
927
|
const namespaces = [.../* @__PURE__ */ new Set([...currentState.namespaces, namespace])];
|
|
697
|
-
const content = await
|
|
928
|
+
const content = await readFile6(namespacePath, "utf8");
|
|
698
929
|
const next = patchBetweenMarkers(
|
|
699
930
|
content,
|
|
700
931
|
namespaceStartMarker,
|
|
701
932
|
namespaceEndMarker,
|
|
702
933
|
`export const namespaces = [${formatArray(namespaces)}] as const;`
|
|
703
934
|
);
|
|
704
|
-
await
|
|
935
|
+
await writeFile6(namespacePath, next, "utf8");
|
|
705
936
|
await writeManifest(projectDir, {
|
|
706
937
|
...currentState,
|
|
707
938
|
namespaces
|
|
@@ -709,8 +940,8 @@ async function appendNamespace(projectDir, namespace) {
|
|
|
709
940
|
return namespaces;
|
|
710
941
|
}
|
|
711
942
|
async function cloneLocaleMessages(projectDir, fromLocale, toLocale) {
|
|
712
|
-
const sourceDir =
|
|
713
|
-
const targetDir =
|
|
943
|
+
const sourceDir = path8.join(projectDir, "messages", fromLocale);
|
|
944
|
+
const targetDir = path8.join(projectDir, "messages", toLocale);
|
|
714
945
|
if (!await pathExists(sourceDir)) {
|
|
715
946
|
return;
|
|
716
947
|
}
|
|
@@ -722,12 +953,12 @@ async function cloneLocaleMessages(projectDir, fromLocale, toLocale) {
|
|
|
722
953
|
if (!entry.isFile() || !entry.name.endsWith(".json")) {
|
|
723
954
|
continue;
|
|
724
955
|
}
|
|
725
|
-
const sourcePath =
|
|
726
|
-
const targetPath =
|
|
956
|
+
const sourcePath = path8.join(sourceDir, entry.name);
|
|
957
|
+
const targetPath = path8.join(targetDir, entry.name);
|
|
727
958
|
const sourceContent = JSON.parse(
|
|
728
|
-
await
|
|
959
|
+
await readFile6(sourcePath, "utf8")
|
|
729
960
|
);
|
|
730
|
-
await
|
|
961
|
+
await writeFile6(
|
|
731
962
|
targetPath,
|
|
732
963
|
`${JSON.stringify(deepCloneValue(sourceContent), null, 2)}
|
|
733
964
|
`,
|
|
@@ -738,12 +969,12 @@ async function cloneLocaleMessages(projectDir, fromLocale, toLocale) {
|
|
|
738
969
|
async function writeNamespaceMessages(projectDir, namespace, viTemplate) {
|
|
739
970
|
const state = await detectProjectState(projectDir);
|
|
740
971
|
for (const locale of state.locales) {
|
|
741
|
-
const localeDir =
|
|
972
|
+
const localeDir = path8.join(projectDir, "messages", locale);
|
|
742
973
|
if (!await pathExists(localeDir)) {
|
|
743
974
|
continue;
|
|
744
975
|
}
|
|
745
|
-
const filePath =
|
|
746
|
-
await
|
|
976
|
+
const filePath = path8.join(localeDir, `${namespace}.json`);
|
|
977
|
+
await writeFile6(
|
|
747
978
|
filePath,
|
|
748
979
|
`${JSON.stringify(deepCloneValue(viTemplate), null, 2)}
|
|
749
980
|
`,
|
|
@@ -752,86 +983,7 @@ async function writeNamespaceMessages(projectDir, namespace, viTemplate) {
|
|
|
752
983
|
}
|
|
753
984
|
}
|
|
754
985
|
|
|
755
|
-
// src/core/
|
|
756
|
-
import { readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
|
|
757
|
-
import path6 from "path";
|
|
758
|
-
var ENABLED_MODULES_START = "<!-- nextcli:enabled-modules:start -->";
|
|
759
|
-
var ENABLED_MODULES_END = "<!-- nextcli:enabled-modules:end -->";
|
|
760
|
-
var MODULE_ENV_START = "<!-- nextcli:module-env:start -->";
|
|
761
|
-
var MODULE_ENV_END = "<!-- nextcli:module-env:end -->";
|
|
762
|
-
function buildModuleSection(moduleId) {
|
|
763
|
-
const module = getModuleById(moduleId);
|
|
764
|
-
if (!module.setupSection) {
|
|
765
|
-
return null;
|
|
766
|
-
}
|
|
767
|
-
return `### Module: ${module.label} (\`${module.id}\`)
|
|
768
|
-
|
|
769
|
-
${module.setupSection.trim()}
|
|
770
|
-
`;
|
|
771
|
-
}
|
|
772
|
-
function formatEnabledModulesLine(moduleIds) {
|
|
773
|
-
if (moduleIds.length === 0) {
|
|
774
|
-
return "**Enabled modules:** none";
|
|
775
|
-
}
|
|
776
|
-
const labels = moduleIds.map((id) => `\`${id}\``).join(", ");
|
|
777
|
-
return `**Enabled modules:** ${labels}`;
|
|
778
|
-
}
|
|
779
|
-
async function updateEnabledModulesLine(content, moduleIds) {
|
|
780
|
-
const startIndex = content.indexOf(ENABLED_MODULES_START);
|
|
781
|
-
const endIndex = content.indexOf(ENABLED_MODULES_END);
|
|
782
|
-
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
|
|
783
|
-
return content;
|
|
784
|
-
}
|
|
785
|
-
const before = content.slice(0, startIndex + ENABLED_MODULES_START.length);
|
|
786
|
-
const after = content.slice(endIndex);
|
|
787
|
-
const line = formatEnabledModulesLine(moduleIds);
|
|
788
|
-
return `${before}
|
|
789
|
-
${line}
|
|
790
|
-
${after}`;
|
|
791
|
-
}
|
|
792
|
-
async function mergeModuleSetupSections(projectDir, moduleIds, allProjectModules) {
|
|
793
|
-
const setupPath = path6.join(projectDir, "SETUP.md");
|
|
794
|
-
if (!await pathExists(setupPath)) {
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
let content = await readFile5(setupPath, "utf8");
|
|
798
|
-
const enabledIds = allProjectModules ?? moduleIds;
|
|
799
|
-
content = await updateEnabledModulesLine(content, enabledIds);
|
|
800
|
-
const sections = moduleIds.map((moduleId) => buildModuleSection(moduleId)).filter((section) => Boolean(section));
|
|
801
|
-
if (sections.length === 0) {
|
|
802
|
-
await writeFile5(setupPath, content, "utf8");
|
|
803
|
-
return;
|
|
804
|
-
}
|
|
805
|
-
const startIndex = content.indexOf(MODULE_ENV_START);
|
|
806
|
-
const endIndex = content.indexOf(MODULE_ENV_END);
|
|
807
|
-
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
|
|
808
|
-
await writeFile5(setupPath, content, "utf8");
|
|
809
|
-
return;
|
|
810
|
-
}
|
|
811
|
-
const before = content.slice(0, startIndex + MODULE_ENV_START.length);
|
|
812
|
-
const after = content.slice(endIndex);
|
|
813
|
-
const existingBlock = content.slice(
|
|
814
|
-
startIndex + MODULE_ENV_START.length,
|
|
815
|
-
endIndex
|
|
816
|
-
);
|
|
817
|
-
let nextBlock = existingBlock.trim();
|
|
818
|
-
for (const section of sections) {
|
|
819
|
-
const header = section.split("\n")[0];
|
|
820
|
-
if (header && nextBlock.includes(header)) {
|
|
821
|
-
continue;
|
|
822
|
-
}
|
|
823
|
-
nextBlock = nextBlock ? `${nextBlock}
|
|
824
|
-
|
|
825
|
-
${section}` : section;
|
|
826
|
-
}
|
|
827
|
-
content = `${before}
|
|
828
|
-
${nextBlock ? `
|
|
829
|
-
${nextBlock}
|
|
830
|
-
` : "\n"}${after}`;
|
|
831
|
-
await writeFile5(setupPath, content, "utf8");
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
// src/commands/add.ts
|
|
986
|
+
// src/core/add-feature.ts
|
|
835
987
|
function toKebabCase(input) {
|
|
836
988
|
return input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
837
989
|
}
|
|
@@ -853,8 +1005,18 @@ function singularizeWord(word) {
|
|
|
853
1005
|
}
|
|
854
1006
|
return word;
|
|
855
1007
|
}
|
|
1008
|
+
function resolveFeatureNames(featureName) {
|
|
1009
|
+
const featureSlug = toKebabCase(featureName);
|
|
1010
|
+
if (!featureSlug) {
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
const featurePascal = toPascalCase(featureSlug);
|
|
1014
|
+
const modelPascal = singularizeWord(featurePascal);
|
|
1015
|
+
const modelDelegate = toCamelCase(modelPascal);
|
|
1016
|
+
return { featureSlug, featurePascal, modelPascal, modelDelegate };
|
|
1017
|
+
}
|
|
856
1018
|
function buildFeatureServicesContent(modelPascal, modelDelegate) {
|
|
857
|
-
return `import prisma from "@/lib/prisma";
|
|
1019
|
+
return `import prisma from "@/lib/db/prisma";
|
|
858
1020
|
|
|
859
1021
|
export async function list${modelPascal}s() {
|
|
860
1022
|
return prisma.${modelDelegate}.findMany({
|
|
@@ -919,7 +1081,7 @@ export type Update${modelPascal}Input = z.infer<typeof update${modelPascal}Schem
|
|
|
919
1081
|
}
|
|
920
1082
|
function buildFeatureHooksContent(featureSlug, modelPascal) {
|
|
921
1083
|
return `import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
922
|
-
import { api } from "@/lib/axios
|
|
1084
|
+
import { api } from "@/lib/api/axios";
|
|
923
1085
|
import type { ApiSuccess } from "@/types";
|
|
924
1086
|
import type { Create${modelPascal}Input, Update${modelPascal}Input } from "../validations";
|
|
925
1087
|
|
|
@@ -1151,7 +1313,7 @@ function buildFeatureMessages(featureName) {
|
|
|
1151
1313
|
};
|
|
1152
1314
|
}
|
|
1153
1315
|
function buildCollectionRouteContent(featureSlug, modelPascal) {
|
|
1154
|
-
return `import { fail, ok } from "@/lib/api
|
|
1316
|
+
return `import { fail, ok } from "@/lib/api/response";
|
|
1155
1317
|
import {
|
|
1156
1318
|
create${modelPascal},
|
|
1157
1319
|
list${modelPascal}s,
|
|
@@ -1187,7 +1349,7 @@ export async function POST(request: Request) {
|
|
|
1187
1349
|
`;
|
|
1188
1350
|
}
|
|
1189
1351
|
function buildItemRouteContent(featureSlug, modelPascal) {
|
|
1190
|
-
return `import { fail, ok } from "@/lib/api
|
|
1352
|
+
return `import { fail, ok } from "@/lib/api/response";
|
|
1191
1353
|
import {
|
|
1192
1354
|
delete${modelPascal},
|
|
1193
1355
|
get${modelPascal}ById,
|
|
@@ -1223,58 +1385,321 @@ export async function PUT(
|
|
|
1223
1385
|
});
|
|
1224
1386
|
}
|
|
1225
1387
|
|
|
1226
|
-
try {
|
|
1227
|
-
const updated = await update${modelPascal}(id, parsed.data);
|
|
1228
|
-
return ok(updated);
|
|
1229
|
-
} catch {
|
|
1230
|
-
return fail("INTERNAL_ERROR", "Failed to update ${featureSlug}.", { status: 500 });
|
|
1388
|
+
try {
|
|
1389
|
+
const updated = await update${modelPascal}(id, parsed.data);
|
|
1390
|
+
return ok(updated);
|
|
1391
|
+
} catch {
|
|
1392
|
+
return fail("INTERNAL_ERROR", "Failed to update ${featureSlug}.", { status: 500 });
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
export async function DELETE(
|
|
1397
|
+
_request: Request,
|
|
1398
|
+
context: { params: Promise<{ id: string }> },
|
|
1399
|
+
) {
|
|
1400
|
+
const { id } = await context.params;
|
|
1401
|
+
try {
|
|
1402
|
+
await delete${modelPascal}(id);
|
|
1403
|
+
return ok({ deleted: true });
|
|
1404
|
+
} catch {
|
|
1405
|
+
return fail("INTERNAL_ERROR", "Failed to delete ${featureSlug}.", { status: 500 });
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
`;
|
|
1409
|
+
}
|
|
1410
|
+
async function appendFeatureModelToPrismaSchema(cwd, modelPascal) {
|
|
1411
|
+
const schemaPath = path9.join(cwd, "prisma", "schema.prisma");
|
|
1412
|
+
if (!await pathExists(schemaPath)) {
|
|
1413
|
+
return "skipped";
|
|
1414
|
+
}
|
|
1415
|
+
const schemaContent = await readFile7(schemaPath, "utf8");
|
|
1416
|
+
const modelRegex = new RegExp(`\\bmodel\\s+${modelPascal}\\b`);
|
|
1417
|
+
if (modelRegex.test(schemaContent)) {
|
|
1418
|
+
return "exists";
|
|
1419
|
+
}
|
|
1420
|
+
const modelBlock = `
|
|
1421
|
+
|
|
1422
|
+
// Example feature model generated by NexTCLI.
|
|
1423
|
+
// Review fields/indexes before running migration on production.
|
|
1424
|
+
model ${modelPascal} {
|
|
1425
|
+
id String @id @default(cuid())
|
|
1426
|
+
name String
|
|
1427
|
+
description String?
|
|
1428
|
+
createdAt DateTime @default(now())
|
|
1429
|
+
updatedAt DateTime @updatedAt
|
|
1430
|
+
}
|
|
1431
|
+
`;
|
|
1432
|
+
await writeFile7(
|
|
1433
|
+
schemaPath,
|
|
1434
|
+
`${schemaContent.trimEnd()}${modelBlock}
|
|
1435
|
+
`,
|
|
1436
|
+
"utf8"
|
|
1437
|
+
);
|
|
1438
|
+
return "added";
|
|
1439
|
+
}
|
|
1440
|
+
async function addFeatureToProject(cwd, featureName) {
|
|
1441
|
+
const names = resolveFeatureNames(featureName);
|
|
1442
|
+
if (!names) {
|
|
1443
|
+
return { ok: false, reason: "invalid-name" };
|
|
1444
|
+
}
|
|
1445
|
+
const { featureSlug, modelPascal, modelDelegate } = names;
|
|
1446
|
+
const srcPath = path9.join(cwd, "src");
|
|
1447
|
+
if (!await pathExists(srcPath)) {
|
|
1448
|
+
return { ok: false, reason: "missing-src" };
|
|
1449
|
+
}
|
|
1450
|
+
const featureRoot = path9.join(cwd, "src/features", featureSlug);
|
|
1451
|
+
if (await pathExists(featureRoot)) {
|
|
1452
|
+
return { ok: false, reason: "already-exists" };
|
|
1453
|
+
}
|
|
1454
|
+
await ensureDir(path9.join(featureRoot, "api"));
|
|
1455
|
+
await ensureDir(path9.join(featureRoot, "components"));
|
|
1456
|
+
await writeFile7(
|
|
1457
|
+
path9.join(featureRoot, "services.ts"),
|
|
1458
|
+
buildFeatureServicesContent(modelPascal, modelDelegate),
|
|
1459
|
+
"utf8"
|
|
1460
|
+
);
|
|
1461
|
+
await writeFile7(
|
|
1462
|
+
path9.join(featureRoot, "validations.ts"),
|
|
1463
|
+
buildFeatureValidationContent(modelPascal),
|
|
1464
|
+
"utf8"
|
|
1465
|
+
);
|
|
1466
|
+
await writeFile7(
|
|
1467
|
+
path9.join(featureRoot, "api", `use-${featureSlug}.ts`),
|
|
1468
|
+
buildFeatureHooksContent(featureSlug, modelPascal),
|
|
1469
|
+
"utf8"
|
|
1470
|
+
);
|
|
1471
|
+
await writeFile7(
|
|
1472
|
+
path9.join(featureRoot, "components", `${featureSlug}-table.tsx`),
|
|
1473
|
+
buildFeatureTableContent(featureSlug, modelPascal),
|
|
1474
|
+
"utf8"
|
|
1475
|
+
);
|
|
1476
|
+
await writeFile7(
|
|
1477
|
+
path9.join(featureRoot, "components", `create-${featureSlug}-dialog.tsx`),
|
|
1478
|
+
buildFeatureDialogContent(featureSlug, modelPascal),
|
|
1479
|
+
"utf8"
|
|
1480
|
+
);
|
|
1481
|
+
const routeFilePath = path9.join(
|
|
1482
|
+
cwd,
|
|
1483
|
+
"src/app/api/v1",
|
|
1484
|
+
featureSlug,
|
|
1485
|
+
"route.ts"
|
|
1486
|
+
);
|
|
1487
|
+
await ensureDir(path9.dirname(routeFilePath));
|
|
1488
|
+
await writeFile7(
|
|
1489
|
+
routeFilePath,
|
|
1490
|
+
buildCollectionRouteContent(featureSlug, modelPascal),
|
|
1491
|
+
"utf8"
|
|
1492
|
+
);
|
|
1493
|
+
const idRoutePath = path9.join(
|
|
1494
|
+
cwd,
|
|
1495
|
+
"src/app/api/v1",
|
|
1496
|
+
featureSlug,
|
|
1497
|
+
"[id]",
|
|
1498
|
+
"route.ts"
|
|
1499
|
+
);
|
|
1500
|
+
await ensureDir(path9.dirname(idRoutePath));
|
|
1501
|
+
await writeFile7(
|
|
1502
|
+
idRoutePath,
|
|
1503
|
+
buildItemRouteContent(featureSlug, modelPascal),
|
|
1504
|
+
"utf8"
|
|
1505
|
+
);
|
|
1506
|
+
const featurePagePath = path9.join(
|
|
1507
|
+
cwd,
|
|
1508
|
+
"src/app/(dashboard)",
|
|
1509
|
+
featureSlug,
|
|
1510
|
+
"page.tsx"
|
|
1511
|
+
);
|
|
1512
|
+
await ensureDir(path9.dirname(featurePagePath));
|
|
1513
|
+
await writeFile7(
|
|
1514
|
+
featurePagePath,
|
|
1515
|
+
buildFeaturePageContent(featureSlug, modelPascal),
|
|
1516
|
+
"utf8"
|
|
1517
|
+
);
|
|
1518
|
+
const manifestState = await detectProjectState(cwd);
|
|
1519
|
+
await writeNamespaceMessages(cwd, featureSlug, buildFeatureMessages(modelPascal));
|
|
1520
|
+
const namespaces = await appendNamespace(cwd, featureSlug);
|
|
1521
|
+
await writeManifest(cwd, {
|
|
1522
|
+
...manifestState,
|
|
1523
|
+
namespaces,
|
|
1524
|
+
features: [.../* @__PURE__ */ new Set([...manifestState.features, featureSlug])]
|
|
1525
|
+
});
|
|
1526
|
+
const schemaStatus = await appendFeatureModelToPrismaSchema(cwd, modelPascal);
|
|
1527
|
+
return { ok: true, featureSlug, modelPascal, schemaStatus };
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// src/commands/add.ts
|
|
1531
|
+
import { readdir as readdir4, readFile as readFile8, writeFile as writeFile8 } from "fs/promises";
|
|
1532
|
+
|
|
1533
|
+
// src/core/prompts.ts
|
|
1534
|
+
import {
|
|
1535
|
+
cancel,
|
|
1536
|
+
confirm,
|
|
1537
|
+
intro,
|
|
1538
|
+
isCancel,
|
|
1539
|
+
multiselect,
|
|
1540
|
+
outro,
|
|
1541
|
+
select,
|
|
1542
|
+
text
|
|
1543
|
+
} from "@clack/prompts";
|
|
1544
|
+
|
|
1545
|
+
// src/core/theme.ts
|
|
1546
|
+
var reset = "\x1B[0m";
|
|
1547
|
+
function paint(code, text2) {
|
|
1548
|
+
return `${code}${text2}${reset}`;
|
|
1549
|
+
}
|
|
1550
|
+
var theme = {
|
|
1551
|
+
cyan: (text2) => paint("\x1B[36m", text2),
|
|
1552
|
+
green: (text2) => paint("\x1B[32m", text2),
|
|
1553
|
+
yellow: (text2) => paint("\x1B[33m", text2),
|
|
1554
|
+
red: (text2) => paint("\x1B[31m", text2),
|
|
1555
|
+
blue: (text2) => paint("\x1B[34m", text2),
|
|
1556
|
+
magenta: (text2) => paint("\x1B[35m", text2),
|
|
1557
|
+
dim: (text2) => paint("\x1B[2m", text2),
|
|
1558
|
+
bold: (text2) => paint("\x1B[1m", text2)
|
|
1559
|
+
};
|
|
1560
|
+
var bannerGradient = [
|
|
1561
|
+
"\x1B[38;5;51m",
|
|
1562
|
+
"\x1B[38;5;45m",
|
|
1563
|
+
"\x1B[38;5;39m",
|
|
1564
|
+
"\x1B[38;5;33m",
|
|
1565
|
+
"\x1B[38;5;27m"
|
|
1566
|
+
];
|
|
1567
|
+
var log = {
|
|
1568
|
+
info(message) {
|
|
1569
|
+
console.log(`${theme.cyan("\u2139")} ${message}`);
|
|
1570
|
+
},
|
|
1571
|
+
success(message) {
|
|
1572
|
+
console.log(`${theme.green("\u2714")} ${message}`);
|
|
1573
|
+
},
|
|
1574
|
+
warn(message) {
|
|
1575
|
+
console.log(`${theme.yellow("\u26A0")} ${message}`);
|
|
1576
|
+
},
|
|
1577
|
+
error(message) {
|
|
1578
|
+
console.error(`${theme.red("\u2716")} ${message}`);
|
|
1579
|
+
},
|
|
1580
|
+
step(message) {
|
|
1581
|
+
console.log(`${theme.blue("\u2192")} ${message}`);
|
|
1582
|
+
},
|
|
1583
|
+
detail(label, value) {
|
|
1584
|
+
console.log(`${theme.dim(`${label}:`)} ${theme.cyan(value)}`);
|
|
1585
|
+
},
|
|
1586
|
+
command(command) {
|
|
1587
|
+
console.log(`${theme.dim(" ")}${theme.magenta(command)}`);
|
|
1588
|
+
}
|
|
1589
|
+
};
|
|
1590
|
+
|
|
1591
|
+
// src/core/prompts.ts
|
|
1592
|
+
function handleCancelled(value) {
|
|
1593
|
+
if (!isCancel(value)) {
|
|
1594
|
+
return;
|
|
1231
1595
|
}
|
|
1596
|
+
cancel(theme.red("Operation cancelled."));
|
|
1597
|
+
process.exit(0);
|
|
1232
1598
|
}
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
_request: Request,
|
|
1236
|
-
context: { params: Promise<{ id: string }> },
|
|
1237
|
-
) {
|
|
1238
|
-
const { id } = await context.params;
|
|
1239
|
-
try {
|
|
1240
|
-
await delete${modelPascal}(id);
|
|
1241
|
-
return ok({ deleted: true });
|
|
1242
|
-
} catch {
|
|
1243
|
-
return fail("INTERNAL_ERROR", "Failed to delete ${featureSlug}.", { status: 500 });
|
|
1244
|
-
}
|
|
1599
|
+
function startPrompt(title) {
|
|
1600
|
+
intro(theme.cyan(theme.bold(title)));
|
|
1245
1601
|
}
|
|
1246
|
-
|
|
1602
|
+
function finishPrompt(message) {
|
|
1603
|
+
outro(theme.green(message));
|
|
1604
|
+
}
|
|
1605
|
+
async function askSelect(message, options, initialValue) {
|
|
1606
|
+
const value = await select({
|
|
1607
|
+
message,
|
|
1608
|
+
options,
|
|
1609
|
+
initialValue
|
|
1610
|
+
});
|
|
1611
|
+
handleCancelled(value);
|
|
1612
|
+
return value;
|
|
1613
|
+
}
|
|
1614
|
+
async function askMultiSelect(message, options, initialValues = []) {
|
|
1615
|
+
const values = await multiselect({
|
|
1616
|
+
message,
|
|
1617
|
+
options,
|
|
1618
|
+
initialValues,
|
|
1619
|
+
required: false
|
|
1620
|
+
});
|
|
1621
|
+
handleCancelled(values);
|
|
1622
|
+
return values;
|
|
1623
|
+
}
|
|
1624
|
+
async function askConfirm(message, initialValue = false) {
|
|
1625
|
+
const value = await confirm({
|
|
1626
|
+
message,
|
|
1627
|
+
initialValue
|
|
1628
|
+
});
|
|
1629
|
+
handleCancelled(value);
|
|
1630
|
+
return Boolean(value);
|
|
1631
|
+
}
|
|
1632
|
+
async function askText(message, options = {}) {
|
|
1633
|
+
const value = await text({
|
|
1634
|
+
message,
|
|
1635
|
+
placeholder: options.placeholder,
|
|
1636
|
+
initialValue: options.initialValue,
|
|
1637
|
+
validate: options.validate
|
|
1638
|
+
});
|
|
1639
|
+
handleCancelled(value);
|
|
1640
|
+
return value;
|
|
1247
1641
|
}
|
|
1248
|
-
async function appendFeatureModelToPrismaSchema(cwd, modelPascal) {
|
|
1249
|
-
const schemaPath = path7.join(cwd, "prisma", "schema.prisma");
|
|
1250
|
-
if (!await pathExists(schemaPath)) {
|
|
1251
|
-
return "skipped";
|
|
1252
|
-
}
|
|
1253
|
-
const schemaContent = await readFile6(schemaPath, "utf8");
|
|
1254
|
-
const modelRegex = new RegExp(`\\bmodel\\s+${modelPascal}\\b`);
|
|
1255
|
-
if (modelRegex.test(schemaContent)) {
|
|
1256
|
-
return "exists";
|
|
1257
|
-
}
|
|
1258
|
-
const modelBlock = `
|
|
1259
1642
|
|
|
1260
|
-
//
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1643
|
+
// src/core/auth-bootstrap.ts
|
|
1644
|
+
import { spawn } from "child_process";
|
|
1645
|
+
async function runCommand(command, args, cwd, silent = true) {
|
|
1646
|
+
return new Promise((resolve) => {
|
|
1647
|
+
const chunks = [];
|
|
1648
|
+
const child = spawn(command, args, {
|
|
1649
|
+
cwd,
|
|
1650
|
+
shell: process.platform === "win32",
|
|
1651
|
+
stdio: silent ? "pipe" : "inherit"
|
|
1652
|
+
});
|
|
1653
|
+
if (silent) {
|
|
1654
|
+
child.stdout?.on("data", (chunk) => chunks.push(String(chunk)));
|
|
1655
|
+
child.stderr?.on("data", (chunk) => chunks.push(String(chunk)));
|
|
1656
|
+
}
|
|
1657
|
+
child.on("error", () => {
|
|
1658
|
+
resolve({ ok: false, output: chunks.join("") });
|
|
1659
|
+
});
|
|
1660
|
+
child.on("close", (code) => {
|
|
1661
|
+
resolve({ ok: code === 0, output: chunks.join("") });
|
|
1662
|
+
});
|
|
1663
|
+
});
|
|
1268
1664
|
}
|
|
1269
|
-
|
|
1270
|
-
await
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
"utf8"
|
|
1665
|
+
async function ensureBetterAuthGenerate(cwd, options = {}) {
|
|
1666
|
+
const localCheck = await runCommand(
|
|
1667
|
+
"npx",
|
|
1668
|
+
["--no-install", "auth", "--help"],
|
|
1669
|
+
cwd
|
|
1275
1670
|
);
|
|
1276
|
-
|
|
1671
|
+
const hasCli = localCheck.ok;
|
|
1672
|
+
let canInstall = hasCli;
|
|
1673
|
+
if (!hasCli) {
|
|
1674
|
+
if (options.nonInteractive) {
|
|
1675
|
+
log.warn(
|
|
1676
|
+
"Better Auth CLI not detected. Skipping schema generation in non-interactive mode."
|
|
1677
|
+
);
|
|
1678
|
+
return;
|
|
1679
|
+
}
|
|
1680
|
+
const approved = await askConfirm(
|
|
1681
|
+
"Better Auth CLI is not installed. Install/use npx auth@latest and run schema generation now?",
|
|
1682
|
+
true
|
|
1683
|
+
);
|
|
1684
|
+
if (!approved) {
|
|
1685
|
+
log.warn("Skipped Better Auth schema generation.");
|
|
1686
|
+
log.command("npx auth@latest generate --config src/lib/auth/server.ts");
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
canInstall = true;
|
|
1690
|
+
}
|
|
1691
|
+
if (!canInstall) {
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
const generateArgs = hasCli ? ["auth", "generate", "--config", "src/lib/auth/server.ts"] : ["auth@latest", "generate", "--config", "src/lib/auth/server.ts"];
|
|
1695
|
+
const result = await runCommand("npx", generateArgs, cwd, false);
|
|
1696
|
+
if (!result.ok) {
|
|
1697
|
+
log.error("Better Auth generate command failed. Run manually:");
|
|
1698
|
+
log.command("npx auth@latest generate --config src/lib/auth/server.ts");
|
|
1699
|
+
}
|
|
1277
1700
|
}
|
|
1701
|
+
|
|
1702
|
+
// src/commands/add.ts
|
|
1278
1703
|
var authProviderStartMarker = "// AUTO_GENERATED_AUTH_PROVIDERS_START";
|
|
1279
1704
|
var authProviderEndMarker = "// AUTO_GENERATED_AUTH_PROVIDERS_END";
|
|
1280
1705
|
function buildProviderSnippet(providers) {
|
|
@@ -1317,131 +1742,29 @@ function patchAuthProviders(content, providers) {
|
|
|
1317
1742
|
${snippet ? `${snippet}
|
|
1318
1743
|
` : ""} $3`);
|
|
1319
1744
|
}
|
|
1320
|
-
function normalizeModuleSelection(moduleIds) {
|
|
1321
|
-
const selected = [...new Set(moduleIds)];
|
|
1322
|
-
const autoAdded = [];
|
|
1323
|
-
if (selected.includes("chat") && !selected.includes("supabase-realtime")) {
|
|
1324
|
-
selected.unshift("supabase-realtime");
|
|
1325
|
-
autoAdded.push("supabase-realtime");
|
|
1326
|
-
}
|
|
1327
|
-
return {
|
|
1328
|
-
selectedModules: [...new Set(selected)],
|
|
1329
|
-
autoAddedModules: autoAdded
|
|
1330
|
-
};
|
|
1331
|
-
}
|
|
1332
1745
|
function registerAddCommand(program2) {
|
|
1333
1746
|
const add = program2.command("add").description("Add modules to an existing app");
|
|
1334
1747
|
add.command("feature").description("Scaffold a feature folder under src/features").argument("<feature-name>", "Feature name").action(async (featureName) => {
|
|
1335
|
-
const featureSlug = toKebabCase(featureName);
|
|
1336
|
-
if (!featureSlug) {
|
|
1337
|
-
log.error("Invalid feature name.");
|
|
1338
|
-
process.exitCode = 1;
|
|
1339
|
-
return;
|
|
1340
|
-
}
|
|
1341
1748
|
const cwd = process.cwd();
|
|
1342
|
-
const
|
|
1343
|
-
if (!
|
|
1344
|
-
|
|
1345
|
-
"
|
|
1346
|
-
)
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
if (await pathExists(featureRoot)) {
|
|
1355
|
-
log.error(`Feature already exists: ${featureRoot}`);
|
|
1749
|
+
const result = await addFeatureToProject(cwd, featureName);
|
|
1750
|
+
if (!result.ok) {
|
|
1751
|
+
if (result.reason === "invalid-name") {
|
|
1752
|
+
log.error("Invalid feature name.");
|
|
1753
|
+
} else if (result.reason === "missing-src") {
|
|
1754
|
+
log.error(
|
|
1755
|
+
"Run this command from your generated Next.js project root (missing ./src)."
|
|
1756
|
+
);
|
|
1757
|
+
} else if (result.reason === "already-exists") {
|
|
1758
|
+
const slug = featureName.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-");
|
|
1759
|
+
log.error(`Feature already exists: ${path10.join(cwd, "src/features", slug)}`);
|
|
1760
|
+
}
|
|
1356
1761
|
process.exitCode = 1;
|
|
1357
1762
|
return;
|
|
1358
1763
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
path7.join(featureRoot, "services.ts"),
|
|
1363
|
-
buildFeatureServicesContent(modelPascal, modelDelegate),
|
|
1364
|
-
"utf8"
|
|
1365
|
-
);
|
|
1366
|
-
await writeFile6(
|
|
1367
|
-
path7.join(featureRoot, "validations.ts"),
|
|
1368
|
-
buildFeatureValidationContent(modelPascal),
|
|
1369
|
-
"utf8"
|
|
1370
|
-
);
|
|
1371
|
-
await writeFile6(
|
|
1372
|
-
path7.join(featureRoot, "api", `use-${featureSlug}.ts`),
|
|
1373
|
-
buildFeatureHooksContent(featureSlug, modelPascal),
|
|
1374
|
-
"utf8"
|
|
1375
|
-
);
|
|
1376
|
-
await writeFile6(
|
|
1377
|
-
path7.join(featureRoot, "components", `${featureSlug}-table.tsx`),
|
|
1378
|
-
buildFeatureTableContent(featureSlug, modelPascal),
|
|
1379
|
-
"utf8"
|
|
1380
|
-
);
|
|
1381
|
-
await writeFile6(
|
|
1382
|
-
path7.join(
|
|
1383
|
-
featureRoot,
|
|
1384
|
-
"components",
|
|
1385
|
-
`create-${featureSlug}-dialog.tsx`
|
|
1386
|
-
),
|
|
1387
|
-
buildFeatureDialogContent(featureSlug, modelPascal),
|
|
1388
|
-
"utf8"
|
|
1389
|
-
);
|
|
1390
|
-
const routeFilePath = path7.join(
|
|
1391
|
-
cwd,
|
|
1392
|
-
"src/app/api/v1",
|
|
1393
|
-
featureSlug,
|
|
1394
|
-
"route.ts"
|
|
1395
|
-
);
|
|
1396
|
-
await ensureDir(path7.dirname(routeFilePath));
|
|
1397
|
-
await writeFile6(
|
|
1398
|
-
routeFilePath,
|
|
1399
|
-
buildCollectionRouteContent(featureSlug, modelPascal),
|
|
1400
|
-
"utf8"
|
|
1401
|
-
);
|
|
1402
|
-
const idRoutePath = path7.join(
|
|
1403
|
-
cwd,
|
|
1404
|
-
"src/app/api/v1",
|
|
1405
|
-
featureSlug,
|
|
1406
|
-
"[id]",
|
|
1407
|
-
"route.ts"
|
|
1408
|
-
);
|
|
1409
|
-
await ensureDir(path7.dirname(idRoutePath));
|
|
1410
|
-
await writeFile6(
|
|
1411
|
-
idRoutePath,
|
|
1412
|
-
buildItemRouteContent(featureSlug, modelPascal),
|
|
1413
|
-
"utf8"
|
|
1414
|
-
);
|
|
1415
|
-
const featurePagePath = path7.join(
|
|
1416
|
-
cwd,
|
|
1417
|
-
"src/app/(dashboard)",
|
|
1418
|
-
featureSlug,
|
|
1419
|
-
"page.tsx"
|
|
1420
|
-
);
|
|
1421
|
-
await ensureDir(path7.dirname(featurePagePath));
|
|
1422
|
-
await writeFile6(
|
|
1423
|
-
featurePagePath,
|
|
1424
|
-
buildFeaturePageContent(featureSlug, modelPascal),
|
|
1425
|
-
"utf8"
|
|
1426
|
-
);
|
|
1427
|
-
const manifestState = await detectProjectState(cwd);
|
|
1428
|
-
await writeNamespaceMessages(
|
|
1429
|
-
cwd,
|
|
1430
|
-
featureSlug,
|
|
1431
|
-
buildFeatureMessages(modelPascal)
|
|
1432
|
-
);
|
|
1433
|
-
const namespaces = await appendNamespace(cwd, featureSlug);
|
|
1434
|
-
await writeManifest(cwd, {
|
|
1435
|
-
...manifestState,
|
|
1436
|
-
namespaces,
|
|
1437
|
-
features: [.../* @__PURE__ */ new Set([...manifestState.features, featureSlug])]
|
|
1438
|
-
});
|
|
1439
|
-
const schemaStatus = await appendFeatureModelToPrismaSchema(
|
|
1440
|
-
cwd,
|
|
1441
|
-
modelPascal
|
|
1764
|
+
const schemaMessage = result.schemaStatus === "added" ? `Model ${result.modelPascal} appended to prisma/schema.prisma` : result.schemaStatus === "exists" ? `Model ${result.modelPascal} already exists in prisma/schema.prisma` : "Skipped prisma/schema.prisma update (file not found)";
|
|
1765
|
+
log.success(
|
|
1766
|
+
`Feature generated with CRUD: src/features/${result.featureSlug}`
|
|
1442
1767
|
);
|
|
1443
|
-
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)";
|
|
1444
|
-
log.success(`Feature generated with CRUD: src/features/${featureSlug}`);
|
|
1445
1768
|
log.info(schemaMessage);
|
|
1446
1769
|
log.warn(
|
|
1447
1770
|
"No migration was executed. Run your migration command manually when ready."
|
|
@@ -1453,8 +1776,8 @@ function registerAddCommand(program2) {
|
|
|
1453
1776
|
).option("--yes", "Skip prompts").action(
|
|
1454
1777
|
async (options) => {
|
|
1455
1778
|
const cwd = process.cwd();
|
|
1456
|
-
const hasSrc = await pathExists(
|
|
1457
|
-
const hasPackageJson = await pathExists(
|
|
1779
|
+
const hasSrc = await pathExists(path10.join(cwd, "src"));
|
|
1780
|
+
const hasPackageJson = await pathExists(path10.join(cwd, "package.json"));
|
|
1458
1781
|
if (!hasSrc || !hasPackageJson) {
|
|
1459
1782
|
log.error(
|
|
1460
1783
|
"Run this command from your generated Next.js project root."
|
|
@@ -1480,9 +1803,7 @@ function registerAddCommand(program2) {
|
|
|
1480
1803
|
continue;
|
|
1481
1804
|
}
|
|
1482
1805
|
if (moduleId === "supabase") {
|
|
1483
|
-
|
|
1484
|
-
"Supabase is now part of the base template; only supabase-realtime is optional."
|
|
1485
|
-
);
|
|
1806
|
+
requestedIds.push("supabase");
|
|
1486
1807
|
continue;
|
|
1487
1808
|
}
|
|
1488
1809
|
if (moduleId === "resend") {
|
|
@@ -1549,61 +1870,31 @@ function registerAddCommand(program2) {
|
|
|
1549
1870
|
finishPrompt("No modules selected.");
|
|
1550
1871
|
return;
|
|
1551
1872
|
}
|
|
1552
|
-
const skippedConflictsByModule = [];
|
|
1553
|
-
let copiedFileCount = 0;
|
|
1554
|
-
for (const moduleId of selectedModules) {
|
|
1555
|
-
const module = getModuleById(moduleId);
|
|
1556
|
-
const copyReport = await copyDirectorySafely(
|
|
1557
|
-
module.templatePath,
|
|
1558
|
-
cwd
|
|
1559
|
-
);
|
|
1560
|
-
copiedFileCount += copyReport.copiedCount;
|
|
1561
|
-
if (copyReport.skippedConflicts.length > 0) {
|
|
1562
|
-
skippedConflictsByModule.push({
|
|
1563
|
-
moduleId,
|
|
1564
|
-
paths: copyReport.skippedConflicts
|
|
1565
|
-
});
|
|
1566
|
-
}
|
|
1567
|
-
}
|
|
1568
|
-
let chatSchemaStatus;
|
|
1569
|
-
if (selectedModules.includes("chat")) {
|
|
1570
|
-
chatSchemaStatus = await ensureChatSchemaInProject(cwd);
|
|
1571
|
-
}
|
|
1572
|
-
const envTargets = [".env", ".env.example", ".env.development"];
|
|
1573
|
-
for (const moduleId of selectedModules) {
|
|
1574
|
-
const module = getModuleById(moduleId);
|
|
1575
|
-
const moduleEnv = moduleId === "email" && emailProvider ? getEmailProviderEnv(emailProvider) : module.env;
|
|
1576
|
-
if (Object.keys(moduleEnv).length === 0) {
|
|
1577
|
-
continue;
|
|
1578
|
-
}
|
|
1579
|
-
for (const envFile of envTargets) {
|
|
1580
|
-
const envPath = path7.join(cwd, envFile);
|
|
1581
|
-
if (await pathExists(envPath)) {
|
|
1582
|
-
await mergeEnvFile(envPath, moduleEnv, {
|
|
1583
|
-
header: `# --- module: ${module.id} ---`
|
|
1584
|
-
});
|
|
1585
|
-
}
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
const dependencyEntries = selectedModules.reduce((acc, moduleId) => {
|
|
1589
|
-
const module = getModuleById(moduleId);
|
|
1590
|
-
const emailDependencies = moduleId === "email" && emailProvider ? getEmailProviderDependencies(emailProvider) : {};
|
|
1591
|
-
return {
|
|
1592
|
-
...acc,
|
|
1593
|
-
...emailDependencies,
|
|
1594
|
-
...module.dependencies ?? {}
|
|
1595
|
-
};
|
|
1596
|
-
}, {});
|
|
1597
|
-
if (Object.keys(dependencyEntries).length > 0) {
|
|
1598
|
-
await addDependencies(
|
|
1599
|
-
path7.join(cwd, "package.json"),
|
|
1600
|
-
dependencyEntries
|
|
1601
|
-
);
|
|
1602
|
-
}
|
|
1603
1873
|
const state = await detectProjectState(cwd);
|
|
1874
|
+
const modulesToInstall = selectedModules.filter(
|
|
1875
|
+
(moduleId) => !state.modules.includes(moduleId)
|
|
1876
|
+
);
|
|
1877
|
+
if (modulesToInstall.length === 0) {
|
|
1878
|
+
finishPrompt("Selected modules are already installed.");
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
const applyResult = await applyModulesToProject({
|
|
1882
|
+
projectDir: cwd,
|
|
1883
|
+
moduleIds: rawModules.length > 0 ? rawModules : selectedModules,
|
|
1884
|
+
emailProvider,
|
|
1885
|
+
safeCopy: true
|
|
1886
|
+
});
|
|
1887
|
+
const skippedConflictsByModule = applyResult.copyReports.filter((item) => item.report.skippedConflicts.length > 0).map((item) => ({
|
|
1888
|
+
moduleId: item.moduleId,
|
|
1889
|
+
paths: item.report.skippedConflicts
|
|
1890
|
+
}));
|
|
1891
|
+
const copiedFileCount = applyResult.copyReports.reduce(
|
|
1892
|
+
(total, item) => total + item.report.copiedCount,
|
|
1893
|
+
0
|
|
1894
|
+
);
|
|
1604
1895
|
const namespaceSet = new Set(state.namespaces);
|
|
1605
|
-
for (const moduleId of selectedModules) {
|
|
1606
|
-
const moduleTemplateMessages =
|
|
1896
|
+
for (const moduleId of applyResult.selectedModules) {
|
|
1897
|
+
const moduleTemplateMessages = path10.join(
|
|
1607
1898
|
getModuleById(moduleId).templatePath,
|
|
1608
1899
|
"messages/vi"
|
|
1609
1900
|
);
|
|
@@ -1620,8 +1911,8 @@ function registerAddCommand(program2) {
|
|
|
1620
1911
|
const namespace = file.name.replace(/\.json$/, "");
|
|
1621
1912
|
namespaceSet.add(namespace);
|
|
1622
1913
|
const templateData = JSON.parse(
|
|
1623
|
-
await
|
|
1624
|
-
|
|
1914
|
+
await readFile8(
|
|
1915
|
+
path10.join(moduleTemplateMessages, file.name),
|
|
1625
1916
|
"utf8"
|
|
1626
1917
|
)
|
|
1627
1918
|
);
|
|
@@ -1629,29 +1920,21 @@ function registerAddCommand(program2) {
|
|
|
1629
1920
|
await appendNamespace(cwd, namespace);
|
|
1630
1921
|
}
|
|
1631
1922
|
}
|
|
1923
|
+
const mergedModules = [
|
|
1924
|
+
.../* @__PURE__ */ new Set([...state.modules, ...applyResult.selectedModules])
|
|
1925
|
+
];
|
|
1632
1926
|
await writeManifest(cwd, {
|
|
1633
1927
|
...state,
|
|
1634
1928
|
namespaces: [...namespaceSet],
|
|
1635
|
-
modules:
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
(moduleId) => moduleId === "resend" ? "email" : moduleId
|
|
1639
|
-
)
|
|
1640
|
-
)
|
|
1641
|
-
]
|
|
1929
|
+
modules: mergedModules,
|
|
1930
|
+
locales: mergedModules.includes("i18n") ? [.../* @__PURE__ */ new Set([...state.locales, "vi"])] : state.locales,
|
|
1931
|
+
features: mergedModules.includes("example") ? [.../* @__PURE__ */ new Set([...state.features, "example"])] : state.features
|
|
1642
1932
|
});
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
[...state.modules, ...selectedModules].map(
|
|
1646
|
-
(moduleId) => moduleId === "resend" ? "email" : moduleId
|
|
1647
|
-
)
|
|
1648
|
-
)
|
|
1649
|
-
];
|
|
1650
|
-
await mergeModuleSetupSections(cwd, selectedModules, mergedModules);
|
|
1651
|
-
finishPrompt(`Added modules: ${selectedModules.join(", ")}`);
|
|
1933
|
+
await mergeModuleDocs(cwd, modulesToInstall, mergedModules);
|
|
1934
|
+
finishPrompt(`Added modules: ${modulesToInstall.join(", ")}`);
|
|
1652
1935
|
log.detail("Copied files", String(copiedFileCount));
|
|
1653
|
-
if (autoAddedModules.length > 0) {
|
|
1654
|
-
log.detail("Auto-added", autoAddedModules.join(", "));
|
|
1936
|
+
if (applyResult.autoAddedModules.length > 0) {
|
|
1937
|
+
log.detail("Auto-added", applyResult.autoAddedModules.join(", "));
|
|
1655
1938
|
}
|
|
1656
1939
|
if (emailProvider) {
|
|
1657
1940
|
log.detail("Email provider", emailProvider);
|
|
@@ -1670,11 +1953,14 @@ function registerAddCommand(program2) {
|
|
|
1670
1953
|
log.detail(`Skipped (${conflict.moduleId})`, `${preview}${suffix}`);
|
|
1671
1954
|
}
|
|
1672
1955
|
}
|
|
1673
|
-
if (chatSchemaStatus === "added") {
|
|
1956
|
+
if (applyResult.chatSchemaStatus === "added") {
|
|
1674
1957
|
log.info(
|
|
1675
1958
|
"Optional chat schema block was appended to prisma/schema.prisma."
|
|
1676
1959
|
);
|
|
1677
1960
|
}
|
|
1961
|
+
if (applyResult.exampleSchemaStatus === "added") {
|
|
1962
|
+
log.info("Example model was appended to prisma/schema.prisma.");
|
|
1963
|
+
}
|
|
1678
1964
|
log.step(
|
|
1679
1965
|
"Next: run your package manager install to apply new dependencies."
|
|
1680
1966
|
);
|
|
@@ -1682,8 +1968,8 @@ function registerAddCommand(program2) {
|
|
|
1682
1968
|
);
|
|
1683
1969
|
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) => {
|
|
1684
1970
|
const cwd = process.cwd();
|
|
1685
|
-
const hasMessages = await pathExists(
|
|
1686
|
-
const hasConfig = await pathExists(
|
|
1971
|
+
const hasMessages = await pathExists(path10.join(cwd, "messages"));
|
|
1972
|
+
const hasConfig = await pathExists(path10.join(cwd, "src/i18n/config.ts"));
|
|
1687
1973
|
if (!hasMessages || !hasConfig) {
|
|
1688
1974
|
log.error(
|
|
1689
1975
|
"Run this command from a generated Next.js project with i18n scaffold."
|
|
@@ -1750,13 +2036,13 @@ function registerAddCommand(program2) {
|
|
|
1750
2036
|
});
|
|
1751
2037
|
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) => {
|
|
1752
2038
|
const cwd = process.cwd();
|
|
1753
|
-
const authFilePath =
|
|
1754
|
-
const hasSrc = await pathExists(
|
|
1755
|
-
const hasPackageJson = await pathExists(
|
|
2039
|
+
const authFilePath = path10.join(cwd, "src/lib/auth/server.ts");
|
|
2040
|
+
const hasSrc = await pathExists(path10.join(cwd, "src"));
|
|
2041
|
+
const hasPackageJson = await pathExists(path10.join(cwd, "package.json"));
|
|
1756
2042
|
const hasAuthFile = await pathExists(authFilePath);
|
|
1757
2043
|
if (!hasSrc || !hasPackageJson || !hasAuthFile) {
|
|
1758
2044
|
log.error(
|
|
1759
|
-
"Run this command from a generated Next.js project with src/lib/auth.ts."
|
|
2045
|
+
"Run this command from a generated Next.js project with src/lib/auth/server.ts."
|
|
1760
2046
|
);
|
|
1761
2047
|
process.exitCode = 1;
|
|
1762
2048
|
return;
|
|
@@ -1791,13 +2077,13 @@ function registerAddCommand(program2) {
|
|
|
1791
2077
|
finishPrompt("No auth providers selected.");
|
|
1792
2078
|
return;
|
|
1793
2079
|
}
|
|
1794
|
-
const authContent = await
|
|
2080
|
+
const authContent = await readFile8(authFilePath, "utf8");
|
|
1795
2081
|
const existingProviders = readConfiguredProviders(authContent);
|
|
1796
2082
|
const mergedProviders = [
|
|
1797
2083
|
.../* @__PURE__ */ new Set([...existingProviders, ...selectedProviders])
|
|
1798
2084
|
];
|
|
1799
2085
|
const nextAuthContent = patchAuthProviders(authContent, mergedProviders);
|
|
1800
|
-
await
|
|
2086
|
+
await writeFile8(authFilePath, nextAuthContent, "utf8");
|
|
1801
2087
|
const envEntries = {};
|
|
1802
2088
|
if (mergedProviders.includes("google")) {
|
|
1803
2089
|
envEntries.GOOGLE_CLIENT_ID = "";
|
|
@@ -1809,7 +2095,7 @@ function registerAddCommand(program2) {
|
|
|
1809
2095
|
}
|
|
1810
2096
|
const envTargets = [".env", ".env.example", ".env.development"];
|
|
1811
2097
|
for (const envFile of envTargets) {
|
|
1812
|
-
const envPath =
|
|
2098
|
+
const envPath = path10.join(cwd, envFile);
|
|
1813
2099
|
if (await pathExists(envPath)) {
|
|
1814
2100
|
await mergeEnvFile(envPath, envEntries, {
|
|
1815
2101
|
header: "# --- auth providers ---"
|
|
@@ -1826,8 +2112,8 @@ function registerAddCommand(program2) {
|
|
|
1826
2112
|
|
|
1827
2113
|
// src/commands/create.ts
|
|
1828
2114
|
import { spawn as spawn2 } from "child_process";
|
|
1829
|
-
import
|
|
1830
|
-
|
|
2115
|
+
import path11 from "path";
|
|
2116
|
+
var CLI_VERSION = "0.7.0";
|
|
1831
2117
|
async function runInstall(packageManager, cwd) {
|
|
1832
2118
|
const installArgsMap = {
|
|
1833
2119
|
npm: ["install"],
|
|
@@ -1858,18 +2144,6 @@ async function runInstall(packageManager, cwd) {
|
|
|
1858
2144
|
function toProjectSlug(input) {
|
|
1859
2145
|
return input.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1860
2146
|
}
|
|
1861
|
-
function normalizeModuleSelection2(moduleIds) {
|
|
1862
|
-
const selected = [...new Set(moduleIds)];
|
|
1863
|
-
const autoAdded = [];
|
|
1864
|
-
if (selected.includes("chat") && !selected.includes("supabase-realtime")) {
|
|
1865
|
-
selected.unshift("supabase-realtime");
|
|
1866
|
-
autoAdded.push("supabase-realtime");
|
|
1867
|
-
}
|
|
1868
|
-
return {
|
|
1869
|
-
selectedModules: [...new Set(selected)],
|
|
1870
|
-
autoAddedModules: autoAdded
|
|
1871
|
-
};
|
|
1872
|
-
}
|
|
1873
2147
|
async function resolveProjectName() {
|
|
1874
2148
|
while (true) {
|
|
1875
2149
|
const projectName = await askText("What is your project name?", {
|
|
@@ -1883,7 +2157,7 @@ async function resolveProjectName() {
|
|
|
1883
2157
|
}
|
|
1884
2158
|
}
|
|
1885
2159
|
});
|
|
1886
|
-
const targetPath =
|
|
2160
|
+
const targetPath = path11.resolve(process.cwd(), projectName);
|
|
1887
2161
|
if (await pathExists(targetPath)) {
|
|
1888
2162
|
log.error(`Target directory already exists: ${targetPath}`);
|
|
1889
2163
|
continue;
|
|
@@ -1895,8 +2169,8 @@ function registerCreateCommand(program2) {
|
|
|
1895
2169
|
program2.command("create").description("Create a new outsource-ready Next.js app").action(async () => {
|
|
1896
2170
|
startPrompt("NexTCLI project creation");
|
|
1897
2171
|
const projectName = await resolveProjectName();
|
|
1898
|
-
const targetPath =
|
|
1899
|
-
const projectDirectoryName =
|
|
2172
|
+
const targetPath = path11.resolve(process.cwd(), projectName);
|
|
2173
|
+
const projectDirectoryName = path11.basename(targetPath);
|
|
1900
2174
|
const projectSlug = toProjectSlug(projectDirectoryName);
|
|
1901
2175
|
const packageManager = await askSelect(
|
|
1902
2176
|
"Which package manager do you want to use?",
|
|
@@ -1921,9 +2195,8 @@ function registerCreateCommand(program2) {
|
|
|
1921
2195
|
})),
|
|
1922
2196
|
[]
|
|
1923
2197
|
);
|
|
1924
|
-
const { selectedModules, autoAddedModules } = normalizeModuleSelection2(rawModules);
|
|
1925
2198
|
let emailProvider;
|
|
1926
|
-
if (
|
|
2199
|
+
if (rawModules.includes("email")) {
|
|
1927
2200
|
emailProvider = await askSelect(
|
|
1928
2201
|
"Select email provider:",
|
|
1929
2202
|
[
|
|
@@ -1943,65 +2216,39 @@ function registerCreateCommand(program2) {
|
|
|
1943
2216
|
}
|
|
1944
2217
|
const shouldInstall = await askConfirm("Install dependencies now?", true);
|
|
1945
2218
|
await copyDirectory(templatePaths.base, targetPath);
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
if (selectedModules.includes("chat")) {
|
|
1952
|
-
chatSchemaStatus = await ensureChatSchemaInProject(targetPath);
|
|
1953
|
-
}
|
|
1954
|
-
const envTargets = [".env", ".env.example", ".env.development"];
|
|
1955
|
-
for (const moduleId of selectedModules) {
|
|
1956
|
-
const module = getModuleById(moduleId);
|
|
1957
|
-
const moduleEnv = moduleId === "email" && emailProvider ? getEmailProviderEnv(emailProvider) : module.env;
|
|
1958
|
-
if (Object.keys(moduleEnv).length === 0) {
|
|
1959
|
-
continue;
|
|
1960
|
-
}
|
|
1961
|
-
for (const envFile of envTargets) {
|
|
1962
|
-
const envPath = path8.join(targetPath, envFile);
|
|
1963
|
-
if (await pathExists(envPath)) {
|
|
1964
|
-
await mergeEnvFile(envPath, moduleEnv, {
|
|
1965
|
-
header: `# --- module: ${module.id} ---`
|
|
1966
|
-
});
|
|
1967
|
-
}
|
|
1968
|
-
}
|
|
1969
|
-
}
|
|
1970
|
-
const dependencyEntries = selectedModules.reduce(
|
|
1971
|
-
(acc, moduleId) => {
|
|
1972
|
-
const module = getModuleById(moduleId);
|
|
1973
|
-
const emailDependencies = moduleId === "email" && emailProvider ? getEmailProviderDependencies(emailProvider) : {};
|
|
1974
|
-
return {
|
|
1975
|
-
...acc,
|
|
1976
|
-
...emailDependencies,
|
|
1977
|
-
...module.dependencies ?? {}
|
|
1978
|
-
};
|
|
1979
|
-
},
|
|
1980
|
-
{}
|
|
1981
|
-
);
|
|
1982
|
-
if (Object.keys(dependencyEntries).length > 0) {
|
|
1983
|
-
await addDependencies(
|
|
1984
|
-
path8.join(targetPath, "package.json"),
|
|
1985
|
-
dependencyEntries
|
|
1986
|
-
);
|
|
1987
|
-
}
|
|
1988
|
-
const betterAuthSecret = randomBytes(32).toString("base64url");
|
|
2219
|
+
const applyResult = await applyModulesToProject({
|
|
2220
|
+
projectDir: targetPath,
|
|
2221
|
+
moduleIds: rawModules,
|
|
2222
|
+
emailProvider
|
|
2223
|
+
});
|
|
1989
2224
|
await replaceTokensInDirectory(targetPath, {
|
|
1990
2225
|
__PROJECT_NAME__: projectSlug,
|
|
1991
|
-
|
|
1992
|
-
__NEXTCLI_VERSION__: "0.6.1"
|
|
2226
|
+
__NEXTCLI_VERSION__: CLI_VERSION
|
|
1993
2227
|
});
|
|
1994
|
-
await
|
|
2228
|
+
await mergeModuleDocs(
|
|
1995
2229
|
targetPath,
|
|
1996
|
-
selectedModules,
|
|
1997
|
-
selectedModules
|
|
2230
|
+
applyResult.selectedModules,
|
|
2231
|
+
applyResult.selectedModules
|
|
1998
2232
|
);
|
|
1999
2233
|
const manifest = await readManifest(targetPath);
|
|
2000
2234
|
if (manifest) {
|
|
2235
|
+
const namespaces = [...manifest.namespaces];
|
|
2236
|
+
if (applyResult.selectedModules.includes("i18n")) {
|
|
2237
|
+
namespaces.push("common");
|
|
2238
|
+
}
|
|
2239
|
+
if (applyResult.selectedModules.includes("auth")) {
|
|
2240
|
+
namespaces.push("auth");
|
|
2241
|
+
}
|
|
2242
|
+
if (applyResult.selectedModules.includes("example")) {
|
|
2243
|
+
namespaces.push("example");
|
|
2244
|
+
}
|
|
2001
2245
|
await writeManifest(targetPath, {
|
|
2002
2246
|
...manifest,
|
|
2003
|
-
cli:
|
|
2004
|
-
modules: selectedModules
|
|
2247
|
+
cli: CLI_VERSION,
|
|
2248
|
+
modules: applyResult.selectedModules,
|
|
2249
|
+
namespaces: [...new Set(namespaces)],
|
|
2250
|
+
locales: applyResult.selectedModules.includes("i18n") ? ["vi"] : manifest.locales,
|
|
2251
|
+
features: applyResult.selectedModules.includes("example") ? ["example"] : manifest.features
|
|
2005
2252
|
});
|
|
2006
2253
|
}
|
|
2007
2254
|
if (shouldInstall) {
|
|
@@ -2015,28 +2262,30 @@ function registerCreateCommand(program2) {
|
|
|
2015
2262
|
}
|
|
2016
2263
|
log.detail(
|
|
2017
2264
|
"Modules",
|
|
2018
|
-
selectedModules.length > 0 ? selectedModules.join(", ") : "none"
|
|
2265
|
+
applyResult.selectedModules.length > 0 ? applyResult.selectedModules.join(", ") : "none"
|
|
2019
2266
|
);
|
|
2020
|
-
if (autoAddedModules.length > 0) {
|
|
2021
|
-
log.detail("Auto-added", autoAddedModules.join(", "));
|
|
2267
|
+
if (applyResult.autoAddedModules.length > 0) {
|
|
2268
|
+
log.detail("Auto-added", applyResult.autoAddedModules.join(", "));
|
|
2022
2269
|
}
|
|
2023
2270
|
if (emailProvider) {
|
|
2024
2271
|
log.detail("Email provider", emailProvider);
|
|
2025
2272
|
}
|
|
2026
|
-
if (chatSchemaStatus === "added") {
|
|
2273
|
+
if (applyResult.chatSchemaStatus === "added") {
|
|
2027
2274
|
log.info(
|
|
2028
2275
|
"Optional chat schema block was appended to prisma/schema.prisma."
|
|
2029
2276
|
);
|
|
2030
2277
|
}
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2278
|
+
if (applyResult.exampleSchemaStatus === "added") {
|
|
2279
|
+
log.info("Example model was appended to prisma/schema.prisma.");
|
|
2280
|
+
}
|
|
2281
|
+
const nextStep = applyResult.selectedModules.includes("database") ? `cd ${projectName} && ${packageManager} run db:migrate && ${packageManager} run dev` : `cd ${projectName} && ${packageManager} run dev`;
|
|
2282
|
+
log.step(`Next: ${nextStep}`);
|
|
2034
2283
|
});
|
|
2035
2284
|
}
|
|
2036
2285
|
|
|
2037
2286
|
// src/commands/migrate.ts
|
|
2038
2287
|
import { spawn as spawn3 } from "child_process";
|
|
2039
|
-
import
|
|
2288
|
+
import path12 from "path";
|
|
2040
2289
|
function createDefaultMigrationName() {
|
|
2041
2290
|
const now = /* @__PURE__ */ new Date();
|
|
2042
2291
|
const y = now.getFullYear();
|
|
@@ -2048,16 +2297,16 @@ function createDefaultMigrationName() {
|
|
|
2048
2297
|
return `auto_${y}${m}${d}${hh}${mm}${ss}`;
|
|
2049
2298
|
}
|
|
2050
2299
|
async function detectPackageManager(cwd) {
|
|
2051
|
-
if (await pathExists(
|
|
2300
|
+
if (await pathExists(path12.join(cwd, "bun.lockb"))) {
|
|
2052
2301
|
return "bun";
|
|
2053
2302
|
}
|
|
2054
|
-
if (await pathExists(
|
|
2303
|
+
if (await pathExists(path12.join(cwd, "bun.lock"))) {
|
|
2055
2304
|
return "bun";
|
|
2056
2305
|
}
|
|
2057
|
-
if (await pathExists(
|
|
2306
|
+
if (await pathExists(path12.join(cwd, "pnpm-lock.yaml"))) {
|
|
2058
2307
|
return "pnpm";
|
|
2059
2308
|
}
|
|
2060
|
-
if (await pathExists(
|
|
2309
|
+
if (await pathExists(path12.join(cwd, "yarn.lock"))) {
|
|
2061
2310
|
return "yarn";
|
|
2062
2311
|
}
|
|
2063
2312
|
return "npm";
|
|
@@ -2092,8 +2341,8 @@ async function runCommand2(command, args, cwd) {
|
|
|
2092
2341
|
function registerMigrateCommand(program2) {
|
|
2093
2342
|
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) => {
|
|
2094
2343
|
const cwd = process.cwd();
|
|
2095
|
-
const hasPackageJson = await pathExists(
|
|
2096
|
-
const hasPrismaSchema = await pathExists(
|
|
2344
|
+
const hasPackageJson = await pathExists(path12.join(cwd, "package.json"));
|
|
2345
|
+
const hasPrismaSchema = await pathExists(path12.join(cwd, "prisma", "schema.prisma"));
|
|
2097
2346
|
if (!hasPackageJson || !hasPrismaSchema) {
|
|
2098
2347
|
log.error(
|
|
2099
2348
|
"Run this command from a generated project root (requires package.json + prisma/schema.prisma)."
|
|
@@ -2245,7 +2494,7 @@ var NexTCLICommand = class _NexTCLICommand extends Command {
|
|
|
2245
2494
|
|
|
2246
2495
|
// src/cli.ts
|
|
2247
2496
|
var program = new NexTCLICommand();
|
|
2248
|
-
program.name("nextcli").description("Scaffold outsource-ready Next.js projects").version("0.
|
|
2497
|
+
program.name("nextcli").description("Scaffold outsource-ready Next.js projects").version("0.7.0");
|
|
2249
2498
|
registerCreateCommand(program);
|
|
2250
2499
|
registerAddCommand(program);
|
|
2251
2500
|
registerMigrateCommand(program);
|