@thinhnguyencth1204/nextcli 0.6.0 → 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.
Files changed (95) hide show
  1. package/README.md +58 -47
  2. package/dist/cli.js +1002 -753
  3. package/package.json +4 -2
  4. package/templates/{next-base/src/lib/axios-instance.ts → features/api/src/lib/api/axios.ts} +7 -2
  5. package/templates/{next-base/src/lib/api-response.ts → features/api/src/lib/api/response.ts} +1 -5
  6. package/templates/{next-base → features/auth}/src/app/(auth)/change-password/layout.tsx +1 -1
  7. package/templates/{next-base → features/auth}/src/app/(auth)/sign-in/layout.tsx +1 -1
  8. package/templates/{next-base → features/auth}/src/app/api/v1/auth/change-password/route.ts +3 -3
  9. package/templates/{next-base → features/auth}/src/app/api/v1/auth/login/route.ts +3 -3
  10. package/templates/{next-base → features/auth}/src/app/api/v1/auth/logout/route.ts +2 -2
  11. package/templates/{next-base → features/auth}/src/app/api/v1/auth/me/route.ts +2 -2
  12. package/templates/{next-base → features/auth}/src/app/api/v1/auth/refresh/route.ts +2 -2
  13. package/templates/{next-base → features/auth}/src/app/api/v1/users/[id]/route.ts +3 -3
  14. package/templates/{next-base → features/auth}/src/app/api/v1/users/route.ts +3 -3
  15. package/templates/{next-base → features/auth}/src/features/auth/components/account-panel.tsx +1 -1
  16. package/templates/{next-base → features/auth}/src/features/auth/components/change-password-form.tsx +1 -1
  17. package/templates/{next-base → features/auth}/src/features/auth/components/sign-in-form.tsx +2 -2
  18. package/templates/{next-base → features/auth}/src/features/users/services.ts +1 -1
  19. package/templates/{next-base → features/auth}/src/instrumentation.ts +1 -1
  20. package/templates/{next-base/src/lib → features/auth/src/lib/auth}/bootstrap.ts +2 -3
  21. package/templates/features/auth/src/lib/auth/index.ts +1 -0
  22. package/templates/{next-base/src/lib → features/auth/src/lib/auth}/rbac.ts +2 -5
  23. package/templates/{next-base/src/lib/auth.ts → features/auth/src/lib/auth/server.ts} +2 -1
  24. package/templates/{next-base → features/auth}/src/lib/constants.ts +3 -0
  25. package/templates/features/chat/src/app/api/v1/chat/route.ts +1 -1
  26. package/templates/features/chat/src/features/chat/api/use-chat-history.ts +1 -1
  27. package/templates/features/chat/src/features/chat/api/use-send-message.ts +1 -1
  28. package/templates/{next-base → features/dashboard}/src/app/(dashboard)/layout.tsx +1 -1
  29. package/templates/features/dashboard/src/app/page.tsx +5 -0
  30. package/templates/{next-base → features/dashboard}/src/components/layout/private/nav-user.tsx +1 -1
  31. package/templates/{next-base → features/database}/prisma/schema.prisma +1 -15
  32. package/templates/{next-base → features/database}/prisma.config.ts +2 -2
  33. package/templates/features/database/src/lib/prisma.ts +23 -0
  34. package/templates/{next-base → features/example}/src/app/api/v1/example/route.ts +2 -2
  35. package/templates/{next-base → features/example}/src/example/api/use-example.ts +1 -1
  36. package/templates/{next-base → features/example}/src/example/api/use-mutations.ts +1 -1
  37. package/templates/{next-base → features/example}/src/example/services.ts +1 -1
  38. package/templates/features/i18n/next.config.ts +17 -0
  39. package/templates/features/i18n/src/app/layout.tsx +42 -0
  40. package/templates/next-base/.env +0 -14
  41. package/templates/next-base/.env.development +0 -14
  42. package/templates/next-base/.env.example +0 -14
  43. package/templates/next-base/PROJECT_STRUCTURE.md +33 -55
  44. package/templates/next-base/SETUP.md +12 -60
  45. package/templates/next-base/bun.lock +17 -0
  46. package/templates/next-base/next.config.ts +1 -4
  47. package/templates/next-base/nextcli.json +3 -3
  48. package/templates/next-base/package.json +1 -21
  49. package/templates/next-base/src/app/layout.tsx +6 -14
  50. package/templates/next-base/src/app/page.tsx +25 -2
  51. package/templates/next-base/prisma/migrations/20260612000000_init/migration.sql +0 -104
  52. package/templates/next-base/prisma/migrations/migration_lock.toml +0 -3
  53. package/templates/next-base/src/app/(auth)/.gitkeep +0 -1
  54. /package/templates/{next-base → features/api}/src/components/providers/query-provider.tsx +0 -0
  55. /package/templates/{next-base/src/lib → features/api/src/lib/api}/token-store.ts +0 -0
  56. /package/templates/{next-base → features/auth}/messages/vi/auth.json +0 -0
  57. /package/templates/{next-base/prisma/migrations → features/auth/src/app/(auth)}/.gitkeep +0 -0
  58. /package/templates/{next-base → features/auth}/src/app/(auth)/change-password/page.tsx +0 -0
  59. /package/templates/{next-base → features/auth}/src/app/(auth)/layout.tsx +0 -0
  60. /package/templates/{next-base → features/auth}/src/app/(auth)/sign-in/page.tsx +0 -0
  61. /package/templates/{next-base → features/auth}/src/app/api/auth/[...all]/route.ts +0 -0
  62. /package/templates/{next-base → features/auth}/src/features/auth/validations.ts +0 -0
  63. /package/templates/{next-base → features/auth}/src/features/users/validations.ts +0 -0
  64. /package/templates/{next-base/src/lib/auth-client.ts → features/auth/src/lib/auth/client.ts} +0 -0
  65. /package/templates/{next-base/src/lib/auth-cookies.ts → features/auth/src/lib/auth/cookies.ts} +0 -0
  66. /package/templates/{next-base → features/dashboard}/src/app/(dashboard)/account/page.tsx +0 -0
  67. /package/templates/{next-base → features/dashboard}/src/app/(dashboard)/dashboard/page.tsx +0 -0
  68. /package/templates/{next-base → features/dashboard}/src/components/layout/private/app-sidebar.tsx +0 -0
  69. /package/templates/{next-base → features/dashboard}/src/components/layout/private/dashboard-layout.tsx +0 -0
  70. /package/templates/{next-base → features/dashboard}/src/components/layout/private/nav-sidebar.tsx +0 -0
  71. /package/templates/{next-base → features/dashboard}/src/components/ui/data-table/data-table-column-header.tsx +0 -0
  72. /package/templates/{next-base → features/dashboard}/src/components/ui/data-table/data-table-filter-list.tsx +0 -0
  73. /package/templates/{next-base → features/dashboard}/src/components/ui/data-table/data-table-pagination.tsx +0 -0
  74. /package/templates/{next-base → features/dashboard}/src/components/ui/data-table/data-table-skeleton.tsx +0 -0
  75. /package/templates/{next-base → features/dashboard}/src/components/ui/data-table/data-table-toolbar.tsx +0 -0
  76. /package/templates/{next-base → features/dashboard}/src/components/ui/data-table/data-table-view-options.tsx +0 -0
  77. /package/templates/{next-base → features/dashboard}/src/components/ui/data-table/data-table.tsx +0 -0
  78. /package/templates/{next-base → features/dashboard}/src/components/ui/sidebar.tsx +0 -0
  79. /package/templates/{next-base → features/dashboard}/src/data/sidebar-modules.ts +0 -0
  80. /package/templates/{next-base → features/dashboard}/src/hooks/table/use-data-table.ts +0 -0
  81. /package/templates/{next-base → features/dashboard}/src/hooks/use-mobile.ts +0 -0
  82. /package/templates/{next-base → features/dashboard}/src/types/data-table.ts +0 -0
  83. /package/templates/{next-base/src/lib → features/database/src/lib/db}/prisma.ts +0 -0
  84. /package/templates/{next-base → features/example}/messages/vi/example.json +0 -0
  85. /package/templates/{next-base → features/example}/src/app/(dashboard)/example/page.tsx +0 -0
  86. /package/templates/{next-base → features/example}/src/example/components/example-table.tsx +0 -0
  87. /package/templates/{next-base → features/example}/src/example/validations.ts +0 -0
  88. /package/templates/{next-base → features/i18n}/messages/vi/common.json +0 -0
  89. /package/templates/{next-base → features/i18n}/src/components/layout/private/locale-switcher.tsx +0 -0
  90. /package/templates/{next-base → features/i18n}/src/i18n/config.ts +0 -0
  91. /package/templates/{next-base → features/i18n}/src/i18n/namespaces.ts +0 -0
  92. /package/templates/{next-base → features/i18n}/src/i18n/request.ts +0 -0
  93. /package/templates/{next-base → features/supabase}/src/lib/supabase/client.ts +0 -0
  94. /package/templates/{next-base → features/supabase}/src/lib/supabase/storage-config.ts +0 -0
  95. /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 path7 from "path";
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 addDependencies(packageJsonPath, dependencies) {
190
+ async function mergePackageJson(packageJsonPath, options) {
191
191
  const packageRaw = await readFile(packageJsonPath, "utf8");
192
192
  const packageJson = JSON.parse(packageRaw);
193
- const current = packageJson.dependencies ?? {};
194
- packageJson.dependencies = {
195
- ...current,
196
- ...dependencies
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/commands/add.ts
207
- import { readdir as readdir4, readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
219
+ // src/core/apply-modules.ts
220
+ import { randomBytes } from "crypto";
221
+ import path6 from "path";
208
222
 
209
- // src/core/templates.ts
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 = path2.dirname(fileURLToPath(import.meta.url));
213
- var rootDir = path2.resolve(currentDir, "../");
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: path2.join(rootDir, "templates/next-base"),
216
- chat: path2.join(rootDir, "templates/features/chat"),
217
- supabaseRealtime: path2.join(rootDir, "templates/features/supabase-realtime"),
218
- seo: path2.join(rootDir, "templates/features/seo"),
219
- email: path2.join(rootDir, "templates/features/email")
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
- NEXT_PUBLIC_SUPABASE_URL: "",
245
- NEXT_PUBLIC_SUPABASE_ANON_KEY: ""
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/prompts.ts
283
- import {
284
- cancel,
285
- confirm,
286
- intro,
287
- isCancel,
288
- multiselect,
289
- outro,
290
- select,
291
- text
292
- } from "@clack/prompts";
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
- // src/core/prompts.ts
341
- function handleCancelled(value) {
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
- // src/core/auth-bootstrap.ts
393
- import { spawn } from "child_process";
394
- async function runCommand(command, args, cwd, silent = true) {
395
- return new Promise((resolve) => {
396
- const chunks = [];
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 ensureBetterAuthGenerate(cwd, options = {}) {
415
- const localCheck = await runCommand("npx", ["--no-install", "auth", "--help"], cwd);
416
- const hasCli = localCheck.ok;
417
- let canInstall = hasCli;
418
- if (!hasCli) {
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
- if (!canInstall) {
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
- const generateArgs = hasCli ? ["auth", "generate", "--config", "src/lib/auth.ts"] : ["auth@latest", "generate", "--config", "src/lib/auth.ts"];
438
- const result = await runCommand("npx", generateArgs, cwd, false);
439
- if (!result.ok) {
440
- log.error("Better Auth generate command failed. Run manually:");
441
- log.command("npx auth@latest generate --config src/lib/auth.ts");
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
- return {
462
- EMAIL_PROVIDER: "resend",
463
- RESEND_API_KEY: "",
464
- RESEND_FROM_EMAIL: ""
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
- return {
475
- resend: "^6.9.2",
476
- "@react-email/components": "^1.0.12",
477
- "react-email": "^4.0.0"
478
- };
479
- }
480
-
481
- // src/core/chat-schema.ts
482
- import path3 from "path";
483
- import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
484
- var chatSchemaBlock = `
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
- enum ChatMessageKind {
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
- const userBlockRegex = /model User \{[\s\S]*?\n\}/m;
541
- const userBlock = schema.match(userBlockRegex)?.[0];
542
- if (!userBlock) {
543
- return schema;
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 relationLines = [];
546
- if (!userBlock.includes("chatParticipants")) {
547
- relationLines.push(" chatParticipants ChatParticipant[]");
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
- if (!userBlock.includes("chatMessages")) {
550
- relationLines.push(" chatMessages ChatMessage[]");
740
+ let chatSchemaStatus;
741
+ if (selectedModules.includes("chat")) {
742
+ chatSchemaStatus = await ensureChatSchemaInProject(projectDir);
551
743
  }
552
- if (relationLines.length === 0) {
553
- return schema;
744
+ let exampleSchemaStatus;
745
+ if (selectedModules.includes("example")) {
746
+ exampleSchemaStatus = await ensureExampleSchemaInProject(projectDir);
554
747
  }
555
- const nextUserBlock = userBlock.replace(/\n\}$/, `
556
- ${relationLines.join("\n")}
557
- }`);
558
- return schema.replace(userBlock, nextUserBlock);
559
- }
560
- async function ensureChatSchemaInProject(projectRoot) {
561
- const schemaPath = path3.join(projectRoot, "prisma", "schema.prisma");
562
- if (!await pathExists(schemaPath)) {
563
- return "skipped";
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
- let schema = await readFile2(schemaPath, "utf8");
566
- schema = ensureUserRelations(schema);
567
- if (schema.includes("model ChatConversation")) {
568
- await writeFile2(schemaPath, schema, "utf8");
569
- return "exists";
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
- const nextSchema = `${schema.trimEnd()}${chatSchemaBlock}
572
- `;
573
- await writeFile2(schemaPath, nextSchema, "utf8");
574
- return "added";
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 path5 from "path";
579
- import { readdir as readdir3, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
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 path4 from "path";
583
- import { readdir as readdir2, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
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.6.0",
814
+ cli: "0.7.0",
586
815
  defaultLocale: "vi",
587
- locales: ["vi"],
588
- namespaces: ["common", "auth", "example"],
816
+ locales: [],
817
+ namespaces: [],
589
818
  modules: [],
590
- features: ["example"]
819
+ features: []
591
820
  };
592
821
  function getManifestPath(projectDir) {
593
- return path4.join(projectDir, "nextcli.json");
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 readFile3(manifestPath, "utf8");
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 writeFile3(
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 = path4.join(projectDir, "messages");
850
+ const messagesDir = path7.join(projectDir, "messages");
622
851
  if (!await pathExists(messagesDir)) {
623
- return ["vi"];
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.length > 0 ? locales.sort() : ["vi"];
856
+ return locales.sort();
628
857
  }
629
858
  async function detectNamespacesFromDisk(projectDir) {
630
- const namespaceFile = path4.join(projectDir, "src/i18n/namespaces.ts");
859
+ const namespaceFile = path7.join(projectDir, "src/i18n/namespaces.ts");
631
860
  if (!await pathExists(namespaceFile)) {
632
- return [...defaultManifest.namespaces];
861
+ return [];
633
862
  }
634
- const content = await readFile3(namespaceFile, "utf8");
863
+ const content = await readFile5(namespaceFile, "utf8");
635
864
  const namespaces = parseConstArray(content, "namespaces");
636
- return namespaces.length > 0 ? namespaces : [...defaultManifest.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((moduleId) => moduleId === "resend" ? "email" : moduleId).filter((moduleId) => moduleId !== "supabase");
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 = path5.join(projectDir, "src/i18n/config.ts");
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 readFile4(configPath, "utf8");
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 writeFile4(configPath, next, "utf8");
919
+ await writeFile6(configPath, next, "utf8");
689
920
  }
690
921
  async function appendNamespace(projectDir, namespace) {
691
- const namespacePath = path5.join(projectDir, "src/i18n/namespaces.ts");
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 readFile4(namespacePath, "utf8");
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 writeFile4(namespacePath, next, "utf8");
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 = path5.join(projectDir, "messages", fromLocale);
713
- const targetDir = path5.join(projectDir, "messages", toLocale);
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 = path5.join(sourceDir, entry.name);
726
- const targetPath = path5.join(targetDir, entry.name);
956
+ const sourcePath = path8.join(sourceDir, entry.name);
957
+ const targetPath = path8.join(targetDir, entry.name);
727
958
  const sourceContent = JSON.parse(
728
- await readFile4(sourcePath, "utf8")
959
+ await readFile6(sourcePath, "utf8")
729
960
  );
730
- await writeFile4(
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 = path5.join(projectDir, "messages", locale);
972
+ const localeDir = path8.join(projectDir, "messages", locale);
742
973
  if (!await pathExists(localeDir)) {
743
974
  continue;
744
975
  }
745
- const filePath = path5.join(localeDir, `${namespace}.json`);
746
- await writeFile4(
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/setup-docs.ts
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-instance";
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-response";
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-response";
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
- export async function DELETE(
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
- // Example feature model generated by NexTCLI.
1261
- // Review fields/indexes before running migration on production.
1262
- model ${modelPascal} {
1263
- id String @id @default(cuid())
1264
- name String
1265
- description String?
1266
- createdAt DateTime @default(now())
1267
- updatedAt DateTime @updatedAt
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 writeFile6(
1271
- schemaPath,
1272
- `${schemaContent.trimEnd()}${modelBlock}
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
- return "added";
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 srcPath = path7.join(cwd, "src");
1343
- if (!await pathExists(srcPath)) {
1344
- log.error(
1345
- "Run this command from your generated Next.js project root (missing ./src)."
1346
- );
1347
- process.exitCode = 1;
1348
- return;
1349
- }
1350
- const featurePascal = toPascalCase(featureSlug);
1351
- const modelPascal = singularizeWord(featurePascal);
1352
- const modelDelegate = toCamelCase(modelPascal);
1353
- const featureRoot = path7.join(cwd, "src/features", featureSlug);
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
- await ensureDir(path7.join(featureRoot, "api"));
1360
- await ensureDir(path7.join(featureRoot, "components"));
1361
- await writeFile6(
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(path7.join(cwd, "src"));
1457
- const hasPackageJson = await pathExists(path7.join(cwd, "package.json"));
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
- log.info(
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 = path7.join(
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 readFile6(
1624
- path7.join(moduleTemplateMessages, file.name),
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
- ...new Set(
1637
- [...state.modules, ...selectedModules].map(
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
- const mergedModules = [
1644
- ...new Set(
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(path7.join(cwd, "messages"));
1686
- const hasConfig = await pathExists(path7.join(cwd, "src/i18n/config.ts"));
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 = path7.join(cwd, "src/lib/auth.ts");
1754
- const hasSrc = await pathExists(path7.join(cwd, "src"));
1755
- const hasPackageJson = await pathExists(path7.join(cwd, "package.json"));
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 readFile6(authFilePath, "utf8");
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 writeFile6(authFilePath, nextAuthContent, "utf8");
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 = path7.join(cwd, envFile);
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 { randomBytes } from "crypto";
1830
- import path8 from "path";
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 = path8.resolve(process.cwd(), projectName);
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 = path8.resolve(process.cwd(), projectName);
1899
- const projectDirectoryName = path8.basename(targetPath);
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 (selectedModules.includes("email")) {
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
- for (const moduleId of selectedModules) {
1947
- const moduleDefinition = getModuleById(moduleId);
1948
- await copyDirectory(moduleDefinition.templatePath, targetPath);
1949
- }
1950
- let chatSchemaStatus;
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
- __BETTER_AUTH_SECRET__: betterAuthSecret,
1992
- __NEXTCLI_VERSION__: "0.6.0"
2226
+ __NEXTCLI_VERSION__: CLI_VERSION
1993
2227
  });
1994
- await mergeModuleSetupSections(
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: "0.6.0",
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
- log.step(
2032
- `Next: cd ${projectName} && ${packageManager} run db:migrate && ${packageManager} run dev`
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 path9 from "path";
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(path9.join(cwd, "bun.lockb"))) {
2300
+ if (await pathExists(path12.join(cwd, "bun.lockb"))) {
2052
2301
  return "bun";
2053
2302
  }
2054
- if (await pathExists(path9.join(cwd, "bun.lock"))) {
2303
+ if (await pathExists(path12.join(cwd, "bun.lock"))) {
2055
2304
  return "bun";
2056
2305
  }
2057
- if (await pathExists(path9.join(cwd, "pnpm-lock.yaml"))) {
2306
+ if (await pathExists(path12.join(cwd, "pnpm-lock.yaml"))) {
2058
2307
  return "pnpm";
2059
2308
  }
2060
- if (await pathExists(path9.join(cwd, "yarn.lock"))) {
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(path9.join(cwd, "package.json"));
2096
- const hasPrismaSchema = await pathExists(path9.join(cwd, "prisma", "schema.prisma"));
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.6.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);