@thinhnguyencth1204/nextcli 0.8.0 → 1.0.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 (112) hide show
  1. package/README.md +27 -24
  2. package/dist/cli.js +168 -107
  3. package/package.json +1 -1
  4. package/templates/features/api/src/lib/api/axios.ts +1 -90
  5. package/templates/features/auth/messages/vi/auth.json +2 -1
  6. package/templates/features/auth/src/app/(auth)/change-password/page.tsx +5 -4
  7. package/templates/features/auth/src/app/(auth)/layout.tsx +2 -5
  8. package/templates/features/auth/src/app/(auth)/sign-in/page.tsx +5 -4
  9. package/templates/features/auth/src/app/api/v1/auth/login/route.ts +24 -29
  10. package/templates/features/auth/src/app/api/v1/auth/logout/route.ts +0 -5
  11. package/templates/features/auth/src/components/layout/auth/auth-shell.tsx +24 -0
  12. package/templates/features/auth/src/features/auth/components/account-panel.tsx +15 -3
  13. package/templates/features/auth/src/features/auth/components/change-password-form.tsx +27 -30
  14. package/templates/features/auth/src/features/auth/components/sign-in-form.tsx +33 -42
  15. package/templates/features/auth/src/lib/auth/client.ts +2 -2
  16. package/templates/features/auth/src/lib/auth/server.ts +2 -2
  17. package/templates/features/dashboard/src/app/(dashboard)/account/page.tsx +9 -7
  18. package/templates/features/dashboard/src/app/(dashboard)/dashboard/page.tsx +24 -10
  19. package/templates/features/dashboard/src/components/layout/private/app-sidebar.tsx +1 -13
  20. package/templates/features/dashboard/src/components/layout/private/dashboard-layout.tsx +31 -22
  21. package/templates/features/dashboard/src/components/layout/private/page-shell.tsx +40 -0
  22. package/templates/features/database/prisma/schema.prisma +1 -0
  23. package/templates/features/example/messages/vi/example.json +11 -1
  24. package/templates/features/example/src/app/(dashboard)/example/page.tsx +92 -3
  25. package/templates/features/example/src/example/components/example-table.tsx +15 -2
  26. package/templates/next-base/.env +16 -0
  27. package/templates/next-base/.env.development +16 -0
  28. package/templates/next-base/.env.example +16 -0
  29. package/templates/next-base/SETUP.md +62 -10
  30. package/templates/next-base/bun.lock +407 -0
  31. package/templates/next-base/messages/vi/auth.json +43 -0
  32. package/templates/next-base/messages/vi/common.json +53 -0
  33. package/templates/next-base/messages/vi/example.json +20 -0
  34. package/templates/next-base/next-env.d.ts +1 -1
  35. package/templates/next-base/next.config.ts +4 -1
  36. package/templates/next-base/nextcli.json +12 -4
  37. package/templates/next-base/package.json +24 -5
  38. package/templates/next-base/prisma/schema.prisma +85 -0
  39. package/templates/next-base/prisma.config.ts +16 -0
  40. package/templates/next-base/src/app/(auth)/.gitkeep +1 -0
  41. package/templates/next-base/src/app/(auth)/change-password/layout.tsx +21 -0
  42. package/templates/next-base/src/app/(auth)/change-password/page.tsx +15 -0
  43. package/templates/next-base/src/app/(auth)/layout.tsx +6 -0
  44. package/templates/next-base/src/app/(auth)/sign-in/layout.tsx +17 -0
  45. package/templates/next-base/src/app/(auth)/sign-in/page.tsx +15 -0
  46. package/templates/next-base/src/app/(dashboard)/account/page.tsx +20 -0
  47. package/templates/next-base/src/app/(dashboard)/dashboard/page.tsx +31 -0
  48. package/templates/next-base/src/app/(dashboard)/example/page.tsx +102 -0
  49. package/templates/next-base/src/app/(dashboard)/layout.tsx +22 -0
  50. package/templates/next-base/src/app/api/auth/[...all]/route.ts +4 -0
  51. package/templates/next-base/src/app/api/v1/auth/change-password/route.ts +55 -0
  52. package/templates/next-base/src/app/api/v1/auth/login/route.ts +65 -0
  53. package/templates/next-base/src/app/api/v1/auth/logout/route.ts +23 -0
  54. package/templates/next-base/src/app/api/v1/auth/me/route.ts +24 -0
  55. package/templates/next-base/src/app/api/v1/example/route.ts +34 -0
  56. package/templates/next-base/src/app/api/v1/users/[id]/route.ts +104 -0
  57. package/templates/next-base/src/app/api/v1/users/route.ts +58 -0
  58. package/templates/next-base/src/app/layout.tsx +14 -6
  59. package/templates/next-base/src/app/page.tsx +2 -25
  60. package/templates/next-base/src/components/branding/logo.tsx +27 -4
  61. package/templates/next-base/src/components/layout/auth/auth-shell.tsx +24 -0
  62. package/templates/next-base/src/components/layout/private/app-sidebar.tsx +32 -0
  63. package/templates/next-base/src/components/layout/private/dashboard-layout.tsx +63 -0
  64. package/templates/next-base/src/components/layout/private/locale-switcher.tsx +45 -0
  65. package/templates/next-base/src/components/layout/private/nav-sidebar.tsx +55 -0
  66. package/templates/next-base/src/components/layout/private/nav-user.tsx +99 -0
  67. package/templates/next-base/src/components/layout/private/page-shell.tsx +40 -0
  68. package/templates/next-base/src/components/providers/query-provider.tsx +17 -0
  69. package/templates/next-base/src/components/ui/data-table/data-table-column-header.tsx +23 -0
  70. package/templates/next-base/src/components/ui/data-table/data-table-filter-list.tsx +3 -0
  71. package/templates/next-base/src/components/ui/data-table/data-table-pagination.tsx +35 -0
  72. package/templates/next-base/src/components/ui/data-table/data-table-skeleton.tsx +11 -0
  73. package/templates/next-base/src/components/ui/data-table/data-table-toolbar.tsx +14 -0
  74. package/templates/next-base/src/components/ui/data-table/data-table-view-options.tsx +3 -0
  75. package/templates/next-base/src/components/ui/data-table/data-table.tsx +72 -0
  76. package/templates/next-base/src/components/ui/sidebar.tsx +215 -0
  77. package/templates/next-base/src/data/sidebar-modules.ts +11 -0
  78. package/templates/next-base/src/example/api/use-example.ts +21 -0
  79. package/templates/next-base/src/example/api/use-mutations.ts +20 -0
  80. package/templates/next-base/src/example/components/example-table.tsx +64 -0
  81. package/templates/next-base/src/example/services.ts +9 -0
  82. package/templates/next-base/src/example/validations.ts +8 -0
  83. package/templates/next-base/src/features/auth/components/account-panel.tsx +92 -0
  84. package/templates/next-base/src/features/auth/components/change-password-form.tsx +79 -0
  85. package/templates/next-base/src/features/auth/components/sign-in-form.tsx +86 -0
  86. package/templates/next-base/src/features/auth/validations.ts +14 -0
  87. package/templates/next-base/src/features/users/services.ts +132 -0
  88. package/templates/next-base/src/features/users/validations.ts +21 -0
  89. package/templates/next-base/src/hooks/table/use-data-table.ts +33 -0
  90. package/templates/next-base/src/hooks/use-mobile.ts +25 -0
  91. package/templates/next-base/src/i18n/config.ts +7 -0
  92. package/templates/next-base/src/i18n/namespaces.ts +5 -0
  93. package/templates/next-base/src/i18n/request.ts +25 -0
  94. package/templates/next-base/src/instrumentation.ts +14 -0
  95. package/templates/next-base/src/lib/api/axios.ts +56 -0
  96. package/templates/next-base/src/lib/api/response.ts +45 -0
  97. package/templates/next-base/src/lib/auth/bootstrap.ts +95 -0
  98. package/templates/next-base/src/lib/auth/client.ts +7 -0
  99. package/templates/next-base/src/lib/auth/index.ts +1 -0
  100. package/templates/next-base/src/lib/auth/rbac.ts +59 -0
  101. package/templates/next-base/src/lib/auth/server.ts +21 -0
  102. package/templates/next-base/src/lib/constants.ts +10 -0
  103. package/templates/next-base/src/lib/db/prisma.ts +23 -0
  104. package/templates/next-base/src/lib/prisma.ts +23 -0
  105. package/templates/next-base/src/lib/supabase/client.ts +6 -0
  106. package/templates/next-base/src/lib/supabase/rich-text-image-sync.ts +28 -0
  107. package/templates/next-base/src/lib/supabase/storage-config.ts +69 -0
  108. package/templates/next-base/src/lib/supabase/storage.ts +164 -0
  109. package/templates/next-base/src/types/data-table.ts +4 -0
  110. package/templates/features/api/src/lib/api/token-store.ts +0 -13
  111. package/templates/features/auth/src/app/api/v1/auth/refresh/route.ts +0 -32
  112. package/templates/features/auth/src/lib/auth/cookies.ts +0 -15
package/README.md CHANGED
@@ -17,7 +17,7 @@ node dist/cli.js --help
17
17
 
18
18
  ## Command: create
19
19
 
20
- Create a new project from the minimal base template:
20
+ Create a new project from the full-stack base template:
21
21
 
22
22
  ```bash
23
23
  node dist/cli.js create
@@ -27,12 +27,12 @@ This command is fully interactive:
27
27
 
28
28
  - enter project name
29
29
  - select package manager in CLI UI (`npm`, `pnpm`, `yarn`, `bun`)
30
- - multi-select optional modules (see module list below)
30
+ - multi-select optional modules (`chat`, `supabase-realtime`, `seo`, `email`)
31
31
  - confirm install step
32
32
  - normalizes project directory name into a safe project slug for generated `package.json` and env placeholders
33
33
  - ships `SETUP.md` and `PROJECT_STRUCTURE.md` in the generated project root
34
34
 
35
- **Default output is minimal:** Next.js shell, branding, theme, UI primitives, and Lexical rich text adapters only. Database, auth, dashboard, and other stacks are added only when selected.
35
+ **Default output includes:** Prisma + Postgres, Supabase client + Storage, Better Auth + RBAC, API client (Axios + React Query), i18n (next-intl), dashboard shell, example CRUD demo, branding, theme, UI primitives, and Lexical rich text adapters. Optional modules are added only when selected.
36
36
 
37
37
  ## Rich text (base)
38
38
 
@@ -43,29 +43,32 @@ All generated projects include Lexical rich text building blocks:
43
43
  - Toolbar: bold/italic/underline, emoji picker, image URL insertion
44
44
  - Demo page: `/blog-demo`
45
45
 
46
- Lexical content is stored as JSON (`SerializedEditorState`). With the `supabase` module, removed Supabase-managed image URLs trigger storage cleanup via `syncRemovedSupabaseRichTextImages`.
46
+ Lexical content is stored as JSON (`SerializedEditorState`). Supabase-managed image URLs trigger storage cleanup via `syncRemovedSupabaseRichTextImages` (included in base).
47
47
 
48
- Available during `create` and `add module`:
48
+ **Included in every project (base):**
49
49
 
50
- | Module | Adds |
51
- | ------------------- | ------------------------------------------------------------- |
52
- | `database` | Prisma schema, client, `DATABASE_URL`, db scripts |
53
- | `supabase` | Supabase browser client + Storage helpers |
54
- | `auth` | Better Auth, sign-in pages, user APIs, bootstrap admin |
55
- | `api` | Axios client, API envelope helpers, React Query provider |
56
- | `i18n` | next-intl config, messages, locale switcher (auto-adds `api`) |
57
- | `dashboard` | Protected dashboard shell, sidebar, data-table UI |
58
- | `example` | Starter CRUD demo + `Example` Prisma model |
59
- | `chat` | Chat routes, hooks, Prisma chat models (+ auto deps) |
60
- | `supabase-realtime` | Realtime channel helpers (+ auto `supabase`) |
61
- | `seo` | robots/sitemap/JSON-LD helpers |
62
- | `email` | Email helper + React Email templates (SMTP or Resend) |
50
+ | Module | Adds |
51
+ | ----------- | -------------------------------------------------------- |
52
+ | `database` | Prisma schema, client, `DATABASE_URL`, db scripts |
53
+ | `supabase` | Supabase browser client + Storage helpers |
54
+ | `auth` | Better Auth, sign-in pages, user APIs, bootstrap admin |
55
+ | `api` | Axios client, API envelope helpers, React Query provider |
56
+ | `i18n` | next-intl config, messages, locale switcher |
57
+ | `dashboard` | Protected dashboard shell, sidebar, data-table UI |
58
+ | `example` | Starter CRUD demo + `Example` Prisma model |
63
59
 
64
- Module dependencies are auto-added (e.g. `auth` `database`, `dashboard` → `auth` + `api` + `i18n`, `chat` → `database` + `supabase-realtime` + `supabase`).
60
+ **Optional** during `create` and `add module`:
65
61
 
66
- ## Auth module (`auth`)
62
+ | Module | Adds |
63
+ | ------------------- | ------------------------------------------------------------------- |
64
+ | `chat` | Chat routes, hooks, Prisma chat models (+ auto `supabase-realtime`) |
65
+ | `supabase-realtime` | Realtime channel helpers |
66
+ | `seo` | robots/sitemap/JSON-LD helpers |
67
+ | `email` | Email helper + React Email templates (SMTP or Resend) |
67
68
 
68
- When selected, generated projects include:
69
+ ## Auth (base)
70
+
71
+ Every generated project includes:
69
72
 
70
73
  - Better Auth + Prisma adapter with JWT + username plugins
71
74
  - username/password sign-in (`/sign-in`) and forced password change (`/change-password`)
@@ -79,7 +82,7 @@ When selected, generated projects include:
79
82
  - `GET /api/v1/auth/me`
80
83
  - `POST /api/v1/auth/change-password`
81
84
 
82
- Axios setup (`api` module):
85
+ Axios setup (included in base):
83
86
 
84
87
  - `publicApi`: public calls
85
88
  - `protectedApi`: bearer token calls with 401 refresh queue + retry
@@ -94,12 +97,12 @@ Project-owned routes under `/api/v1/*` use a unified envelope:
94
97
  - Error:
95
98
  - `{ success: false, error: { code, message, details? }, timestamp, requestId? }`
96
99
 
97
- This is powered by `src/lib/api/response.ts` when the `api` module is enabled.
100
+ This is powered by `src/lib/api/response.ts` (included in base).
98
101
  The Better Auth passthrough route `/api/auth/[...all]` remains unwrapped.
99
102
 
100
103
  ## Command: add feature
101
104
 
102
- Run inside a generated project root (requires `database` and `api` for full CRUD wiring):
105
+ Run inside a generated project root (database and api are included in base):
103
106
 
104
107
  ```bash
105
108
  node ../dist/cli.js add feature orders
package/dist/cli.js CHANGED
@@ -152,17 +152,40 @@ async function mergeEnvFile(envFilePath, entries, options = {}) {
152
152
  existingKeys.add(key.trim());
153
153
  }
154
154
  }
155
+ let nextLines = [...lines];
155
156
  const additions = [];
156
157
  for (const [key, value] of Object.entries(entries)) {
158
+ if (options.overwrite && existingKeys.has(key)) {
159
+ nextLines = nextLines.map((line) => {
160
+ const trimmed = line.trim();
161
+ if (!trimmed || trimmed.startsWith("#")) {
162
+ return line;
163
+ }
164
+ const [lineKey] = trimmed.split("=");
165
+ if (lineKey?.trim() === key) {
166
+ return `${key}=${formatEnvValue(value)}`;
167
+ }
168
+ return line;
169
+ });
170
+ continue;
171
+ }
157
172
  if (!existingKeys.has(key)) {
158
173
  additions.push(`${key}=${formatEnvValue(value)}`);
159
174
  }
160
175
  }
176
+ let content = currentContent;
177
+ if (options.overwrite) {
178
+ content = `${nextLines.join("\n").replace(/\n?$/, "\n")}`;
179
+ if (additions.length === 0) {
180
+ await writeFile(envFilePath, content, "utf8");
181
+ return;
182
+ }
183
+ }
161
184
  if (additions.length === 0) {
162
185
  return;
163
186
  }
164
- if (options.header && currentContent.includes(options.header)) {
165
- const nextContent2 = currentContent.replace(
187
+ if (options.header && content.includes(options.header)) {
188
+ const nextContent2 = content.replace(
166
189
  options.header,
167
190
  `${options.header}
168
191
  ${additions.join("\n")}`
@@ -173,8 +196,8 @@ ${additions.join("\n")}`
173
196
  if (options.header) {
174
197
  additions.unshift(options.header);
175
198
  }
176
- const separator = currentContent.endsWith("\n") || currentContent.length === 0 ? "" : "\n";
177
- const nextContent = `${currentContent}${separator}${additions.join("\n")}
199
+ const separator = content.endsWith("\n") || content.length === 0 ? "" : "\n";
200
+ const nextContent = `${content}${separator}${additions.join("\n")}
178
201
  `;
179
202
  await writeFile(envFilePath, nextContent, "utf8");
180
203
  }
@@ -382,53 +405,6 @@ function getEmailProviderDependencies(provider) {
382
405
  };
383
406
  }
384
407
 
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
408
  // src/core/templates.ts
433
409
  import path4 from "path";
434
410
  import { existsSync } from "fs";
@@ -453,7 +429,22 @@ var templatePaths = {
453
429
  };
454
430
 
455
431
  // src/core/modules.ts
456
- var optionalModules = [
432
+ var baseModuleIds = [
433
+ "database",
434
+ "supabase",
435
+ "auth",
436
+ "api",
437
+ "i18n",
438
+ "dashboard",
439
+ "example"
440
+ ];
441
+ var optionalModuleIds = [
442
+ "chat",
443
+ "supabase-realtime",
444
+ "seo",
445
+ "email"
446
+ ];
447
+ var moduleDefinitions = [
457
448
  {
458
449
  id: "database",
459
450
  label: "Database (Prisma + Postgres)",
@@ -523,7 +514,7 @@ Create the bucket and RLS policies in Supabase Dashboard before uploads.`
523
514
  | \`BETTER_AUTH_SECRET\` | Auto-generated on create; rotate in production |
524
515
  | \`BETTER_AUTH_URL\` | Your app URL (e.g. \`http://localhost:3000\`) |
525
516
 
526
- Requires \`database\` (auto-added). Bootstrap seeds \`admin\` / \`admin1234\` on first dev start.`
517
+ Requires \`database\` (included in base). Bootstrap seeds \`admin\` / \`admin1234\` on first dev start.`
527
518
  },
528
519
  {
529
520
  id: "api",
@@ -567,7 +558,7 @@ Add more locales with \`nextcli add language\`.`
567
558
  nuqs: "^2.8.1",
568
559
  "date-fns": "^3.6.0"
569
560
  },
570
- setupSection: `Requires \`auth\`, \`api\`, and \`i18n\` (auto-added). Protected routes redirect unauthenticated users to \`/sign-in\`.`
561
+ setupSection: `Requires \`auth\`, \`api\`, and \`i18n\` (included in base). Protected routes redirect unauthenticated users to \`/sign-in\`.`
571
562
  },
572
563
  {
573
564
  id: "example",
@@ -575,7 +566,7 @@ Add more locales with \`nextcli add language\`.`
575
566
  description: "Adds starter example feature with API route, hooks, and Prisma model",
576
567
  templatePath: templatePaths.example,
577
568
  env: {},
578
- setupSection: `Requires \`dashboard\` and \`database\` (auto-added). Appends \`Example\` model to \`prisma/schema.prisma\` \u2014 run \`db:migrate\` after add.`
569
+ setupSection: `Requires \`dashboard\` and \`database\` (included in base). Includes demo \`Example\` model in \`prisma/schema.prisma\` \u2014 run \`db:migrate\` after create.`
579
570
  },
580
571
  {
581
572
  id: "chat",
@@ -589,7 +580,7 @@ Add more locales with \`nextcli add language\`.`
589
580
  | -------- | ------------ |
590
581
  | \`NEXT_PUBLIC_ENABLE_CHAT\` | Set \`true\` when chat module is enabled (auto on add) |
591
582
 
592
- Requires \`database\`, \`supabase\`, and \`supabase-realtime\` (auto-added). Run \`db:migrate\` after add \u2014 chat Prisma models are appended.`
583
+ Requires \`database\`, \`supabase\`, and \`supabase-realtime\` (base includes database/supabase; realtime auto-added). Run \`db:migrate\` after add \u2014 chat Prisma models are appended.`
593
584
  },
594
585
  {
595
586
  id: "supabase-realtime",
@@ -598,7 +589,7 @@ Requires \`database\`, \`supabase\`, and \`supabase-realtime\` (auto-added). Run
598
589
  templatePath: templatePaths.supabaseRealtime,
599
590
  env: {},
600
591
  dependencies: {},
601
- setupSection: `Requires \`supabase\` (auto-added). Enable Realtime on tables in Supabase Dashboard \u2192 Database \u2192 Replication.`
592
+ setupSection: `Requires \`supabase\` (included in base). Enable Realtime on tables in Supabase Dashboard \u2192 Database \u2192 Replication.`
602
593
  },
603
594
  {
604
595
  id: "seo",
@@ -622,13 +613,58 @@ Requires \`database\`, \`supabase\`, and \`supabase-realtime\` (auto-added). Run
622
613
  Only the selected provider keys are merged into your env files.`
623
614
  }
624
615
  ];
616
+ var optionalModules = moduleDefinitions.filter(
617
+ (module) => optionalModuleIds.includes(module.id)
618
+ );
625
619
  function getModuleById(moduleId) {
626
- const module = optionalModules.find((item) => item.id === moduleId);
620
+ const module = moduleDefinitions.find((item) => item.id === moduleId);
627
621
  if (!module) {
628
- throw new Error(`Unknown optional module: ${moduleId}`);
622
+ throw new Error(`Unknown module: ${moduleId}`);
629
623
  }
630
624
  return module;
631
625
  }
626
+ function isBaseModuleId(moduleId) {
627
+ return baseModuleIds.includes(moduleId);
628
+ }
629
+ function isOptionalModuleId(moduleId) {
630
+ return optionalModuleIds.includes(moduleId);
631
+ }
632
+
633
+ // src/core/module-selection.ts
634
+ var MODULE_ORDER = [
635
+ ...baseModuleIds,
636
+ "supabase-realtime",
637
+ "chat",
638
+ "seo",
639
+ "email"
640
+ ];
641
+ var MODULE_DEPENDENCIES = {
642
+ chat: ["supabase-realtime"]
643
+ };
644
+ function sortModules(moduleIds) {
645
+ const set = new Set(moduleIds);
646
+ return MODULE_ORDER.filter((id) => set.has(id));
647
+ }
648
+ function normalizeModuleSelection(moduleIds) {
649
+ const requested = /* @__PURE__ */ new Set([...baseModuleIds, ...moduleIds]);
650
+ const autoAdded = [];
651
+ function addDependencies(moduleId) {
652
+ for (const dep of MODULE_DEPENDENCIES[moduleId] ?? []) {
653
+ if (!requested.has(dep)) {
654
+ requested.add(dep);
655
+ autoAdded.push(dep);
656
+ addDependencies(dep);
657
+ }
658
+ }
659
+ }
660
+ for (const moduleId of moduleIds) {
661
+ addDependencies(moduleId);
662
+ }
663
+ return {
664
+ selectedModules: sortModules([...requested]),
665
+ autoAddedModules: autoAdded
666
+ };
667
+ }
632
668
 
633
669
  // src/core/setup-docs.ts
634
670
  import { readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
@@ -721,12 +757,28 @@ function resolveModuleEnv(moduleId, emailProvider, authSecret) {
721
757
  }
722
758
  return env;
723
759
  }
760
+ async function mergeAuthSecret(projectDir, authSecret) {
761
+ const envTargets = [".env", ".env.example", ".env.development"];
762
+ for (const envFile of envTargets) {
763
+ const envPath = path6.join(projectDir, envFile);
764
+ if (await pathExists(envPath)) {
765
+ await mergeEnvFile(
766
+ envPath,
767
+ { BETTER_AUTH_SECRET: authSecret },
768
+ { header: "# --- module: auth ---", overwrite: true }
769
+ );
770
+ }
771
+ }
772
+ }
724
773
  async function applyModulesToProject(options) {
725
774
  const { projectDir, moduleIds, emailProvider, safeCopy = false } = options;
726
775
  const authSecret = options.authSecret ?? randomBytes(32).toString("base64url");
727
776
  const { selectedModules, autoAddedModules } = normalizeModuleSelection(moduleIds);
728
777
  const copyReports = [];
729
- for (const moduleId of selectedModules) {
778
+ const modulesToCopy = selectedModules.filter(
779
+ (moduleId) => !baseModuleIds.includes(moduleId)
780
+ );
781
+ for (const moduleId of modulesToCopy) {
730
782
  const module = getModuleById(moduleId);
731
783
  if (safeCopy) {
732
784
  copyReports.push({
@@ -742,11 +794,12 @@ async function applyModulesToProject(options) {
742
794
  chatSchemaStatus = await ensureChatSchemaInProject(projectDir);
743
795
  }
744
796
  let exampleSchemaStatus;
745
- if (selectedModules.includes("example")) {
797
+ if (selectedModules.includes("example") && modulesToCopy.includes("example")) {
746
798
  exampleSchemaStatus = await ensureExampleSchemaInProject(projectDir);
747
799
  }
748
800
  const envTargets = [".env", ".env.example", ".env.development"];
749
- for (const moduleId of selectedModules) {
801
+ const envModules = ["auth", ...modulesToCopy];
802
+ for (const moduleId of envModules) {
750
803
  const moduleEnv = resolveModuleEnv(moduleId, emailProvider, authSecret);
751
804
  if (Object.keys(moduleEnv).length === 0) {
752
805
  continue;
@@ -761,11 +814,11 @@ async function applyModulesToProject(options) {
761
814
  }
762
815
  }
763
816
  const packageJsonPath = path6.join(projectDir, "package.json");
764
- if (await pathExists(packageJsonPath)) {
817
+ if (await pathExists(packageJsonPath) && modulesToCopy.length > 0) {
765
818
  const dependencies = {};
766
819
  const devDependencies = {};
767
820
  const scripts = {};
768
- for (const moduleId of selectedModules) {
821
+ for (const moduleId of modulesToCopy) {
769
822
  const module = getModuleById(moduleId);
770
823
  Object.assign(dependencies, module.dependencies ?? {});
771
824
  Object.assign(devDependencies, module.devDependencies ?? {});
@@ -811,12 +864,20 @@ import { readdir as readdir3, readFile as readFile6, writeFile as writeFile6 } f
811
864
  import path7 from "path";
812
865
  import { readdir as readdir2, readFile as readFile5, writeFile as writeFile5 } from "fs/promises";
813
866
  var defaultManifest = {
814
- cli: "0.8.0",
867
+ cli: "1.0.0",
815
868
  defaultLocale: "vi",
816
- locales: [],
817
- namespaces: [],
818
- modules: [],
819
- features: []
869
+ locales: ["vi"],
870
+ namespaces: ["common", "auth", "example"],
871
+ modules: [
872
+ "database",
873
+ "supabase",
874
+ "auth",
875
+ "api",
876
+ "i18n",
877
+ "dashboard",
878
+ "example"
879
+ ],
880
+ features: ["example"]
820
881
  };
821
882
  function getManifestPath(projectDir) {
822
883
  return path7.join(projectDir, "nextcli.json");
@@ -1756,7 +1817,9 @@ function registerAddCommand(program2) {
1756
1817
  );
1757
1818
  } else if (result.reason === "already-exists") {
1758
1819
  const slug = featureName.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-");
1759
- log.error(`Feature already exists: ${path10.join(cwd, "src/features", slug)}`);
1820
+ log.error(
1821
+ `Feature already exists: ${path10.join(cwd, "src/features", slug)}`
1822
+ );
1760
1823
  }
1761
1824
  process.exitCode = 1;
1762
1825
  return;
@@ -1785,9 +1848,6 @@ function registerAddCommand(program2) {
1785
1848
  process.exitCode = 1;
1786
1849
  return;
1787
1850
  }
1788
- const validIds = new Set(
1789
- optionalModules.map((module) => module.id)
1790
- );
1791
1851
  const requestedValues = options.module ? options.module.flatMap((value) => value.split(",")).map((value) => value.trim()).filter(Boolean) : [];
1792
1852
  const requestedIds = [];
1793
1853
  let emailProviderFromModuleAlias;
@@ -1798,14 +1858,6 @@ function registerAddCommand(program2) {
1798
1858
  return;
1799
1859
  }
1800
1860
  for (const moduleId of requestedValues) {
1801
- if (validIds.has(moduleId)) {
1802
- requestedIds.push(moduleId);
1803
- continue;
1804
- }
1805
- if (moduleId === "supabase") {
1806
- requestedIds.push("supabase");
1807
- continue;
1808
- }
1809
1861
  if (moduleId === "resend") {
1810
1862
  log.info(
1811
1863
  "Module 'resend' is now 'email'; selecting email with provider resend."
@@ -1814,11 +1866,18 @@ function registerAddCommand(program2) {
1814
1866
  emailProviderFromModuleAlias = "resend";
1815
1867
  continue;
1816
1868
  }
1817
- if (!validIds.has(moduleId)) {
1818
- log.error(`Unknown module: ${moduleId}`);
1869
+ if (!isOptionalModuleId(moduleId)) {
1870
+ if (isBaseModuleId(moduleId)) {
1871
+ log.error(
1872
+ `Module '${moduleId}' is included in the base template and cannot be added separately.`
1873
+ );
1874
+ } else {
1875
+ log.error(`Unknown module: ${moduleId}`);
1876
+ }
1819
1877
  process.exitCode = 1;
1820
1878
  return;
1821
1879
  }
1880
+ requestedIds.push(moduleId);
1822
1881
  }
1823
1882
  startPrompt("NexTCLI optional modules");
1824
1883
  const rawModules = requestedIds.length > 0 ? [...new Set(requestedIds)] : options.yes ? [] : await askMultiSelect(
@@ -1872,15 +1931,18 @@ function registerAddCommand(program2) {
1872
1931
  }
1873
1932
  const state = await detectProjectState(cwd);
1874
1933
  const modulesToInstall = selectedModules.filter(
1875
- (moduleId) => !state.modules.includes(moduleId)
1934
+ (moduleId) => !state.modules.includes(moduleId) && !baseModuleIds.includes(moduleId)
1876
1935
  );
1877
1936
  if (modulesToInstall.length === 0) {
1878
1937
  finishPrompt("Selected modules are already installed.");
1879
1938
  return;
1880
1939
  }
1940
+ const optionalToApply = rawModules.length > 0 ? rawModules : modulesToInstall.filter(
1941
+ (moduleId) => isOptionalModuleId(moduleId)
1942
+ );
1881
1943
  const applyResult = await applyModulesToProject({
1882
1944
  projectDir: cwd,
1883
- moduleIds: rawModules.length > 0 ? rawModules : selectedModules,
1945
+ moduleIds: optionalToApply,
1884
1946
  emailProvider,
1885
1947
  safeCopy: true
1886
1948
  });
@@ -1927,7 +1989,7 @@ function registerAddCommand(program2) {
1927
1989
  ...state,
1928
1990
  namespaces: [...namespaceSet],
1929
1991
  modules: mergedModules,
1930
- locales: mergedModules.includes("i18n") ? [.../* @__PURE__ */ new Set([...state.locales, "vi"])] : state.locales,
1992
+ locales: state.locales.length > 0 ? state.locales : ["vi"],
1931
1993
  features: mergedModules.includes("example") ? [.../* @__PURE__ */ new Set([...state.features, "example"])] : state.features
1932
1994
  });
1933
1995
  await mergeModuleDocs(cwd, modulesToInstall, mergedModules);
@@ -2111,9 +2173,10 @@ function registerAddCommand(program2) {
2111
2173
  }
2112
2174
 
2113
2175
  // src/commands/create.ts
2176
+ import { randomBytes as randomBytes2 } from "crypto";
2114
2177
  import { spawn as spawn2 } from "child_process";
2115
2178
  import path11 from "path";
2116
- var CLI_VERSION = "0.8.0";
2179
+ var CLI_VERSION = "1.0.0";
2117
2180
  async function runInstall(packageManager, cwd) {
2118
2181
  const installArgsMap = {
2119
2182
  npm: ["install"],
@@ -2215,11 +2278,14 @@ function registerCreateCommand(program2) {
2215
2278
  );
2216
2279
  }
2217
2280
  const shouldInstall = await askConfirm("Install dependencies now?", true);
2281
+ const authSecret = randomBytes2(32).toString("base64url");
2218
2282
  await copyDirectory(templatePaths.base, targetPath);
2283
+ await mergeAuthSecret(targetPath, authSecret);
2219
2284
  const applyResult = await applyModulesToProject({
2220
2285
  projectDir: targetPath,
2221
2286
  moduleIds: rawModules,
2222
- emailProvider
2287
+ emailProvider,
2288
+ authSecret
2223
2289
  });
2224
2290
  await replaceTokensInDirectory(targetPath, {
2225
2291
  __PROJECT_NAME__: projectSlug,
@@ -2227,28 +2293,18 @@ function registerCreateCommand(program2) {
2227
2293
  });
2228
2294
  await mergeModuleDocs(
2229
2295
  targetPath,
2230
- applyResult.selectedModules,
2296
+ [...baseModuleIds, ...rawModules],
2231
2297
  applyResult.selectedModules
2232
2298
  );
2233
2299
  const manifest = await readManifest(targetPath);
2234
2300
  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
- }
2245
2301
  await writeManifest(targetPath, {
2246
2302
  ...manifest,
2247
2303
  cli: CLI_VERSION,
2248
2304
  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
2305
+ namespaces: ["common", "auth", "example"],
2306
+ locales: ["vi"],
2307
+ features: ["example"]
2252
2308
  });
2253
2309
  }
2254
2310
  if (shouldInstall) {
@@ -2260,9 +2316,13 @@ function registerCreateCommand(program2) {
2260
2316
  if (projectSlug !== projectDirectoryName) {
2261
2317
  log.detail("Normalized project id", projectSlug);
2262
2318
  }
2319
+ log.detail("Base modules", baseModuleIds.join(", "));
2320
+ const optionalInstalled = applyResult.selectedModules.filter(
2321
+ (moduleId) => !baseModuleIds.includes(moduleId)
2322
+ );
2263
2323
  log.detail(
2264
- "Modules",
2265
- applyResult.selectedModules.length > 0 ? applyResult.selectedModules.join(", ") : "none"
2324
+ "Optional modules",
2325
+ optionalInstalled.length > 0 ? optionalInstalled.join(", ") : "none"
2266
2326
  );
2267
2327
  if (applyResult.autoAddedModules.length > 0) {
2268
2328
  log.detail("Auto-added", applyResult.autoAddedModules.join(", "));
@@ -2278,8 +2338,9 @@ function registerCreateCommand(program2) {
2278
2338
  if (applyResult.exampleSchemaStatus === "added") {
2279
2339
  log.info("Example model was appended to prisma/schema.prisma.");
2280
2340
  }
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}`);
2341
+ log.step(
2342
+ `Next: cd ${projectName} && ${packageManager} run db:migrate && ${packageManager} run dev`
2343
+ );
2283
2344
  });
2284
2345
  }
2285
2346
 
@@ -2494,7 +2555,7 @@ var NexTCLICommand = class _NexTCLICommand extends Command {
2494
2555
 
2495
2556
  // src/cli.ts
2496
2557
  var program = new NexTCLICommand();
2497
- program.name("nextcli").description("Scaffold outsource-ready Next.js projects").version("0.8.0");
2558
+ program.name("nextcli").description("Scaffold outsource-ready Next.js projects").version("1.0.0");
2498
2559
  registerCreateCommand(program);
2499
2560
  registerAddCommand(program);
2500
2561
  registerMigrateCommand(program);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thinhnguyencth1204/nextcli",
3
- "version": "0.8.0",
3
+ "version": "1.0.0",
4
4
  "description": "CLI scaffolder for outsourced Next.js projects",
5
5
  "type": "module",
6
6
  "bin": {