@fernir2/saas-kit-cli 0.1.33 → 0.1.35

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 (260) hide show
  1. package/base-repo/app-constants/aliases.cjs.js +1 -1
  2. package/base-repo/app-constants/aliases.js +1 -1
  3. package/base-repo/app-constants/project-paths.cjs.js +1 -1
  4. package/base-repo/app-constants/project-paths.js +1 -1
  5. package/base-repo/constants/create-app-constants.cjs.js +1 -1
  6. package/base-repo/constants/create-app-constants.js +1 -1
  7. package/base-repo/constants/packages.cjs.js +1 -1
  8. package/base-repo/constants/packages.cjs2.js +1 -1
  9. package/base-repo/constants/packages.js +1 -1
  10. package/base-repo/constants/packages2.js +1 -1
  11. package/cli/.env.example +61 -55
  12. package/cli/README.md +3 -101
  13. package/cli/bin/{index.ts → create.ts} +444 -383
  14. package/cli/configs/drizzle-cli-config.ts +14 -0
  15. package/cli/configs/playwright-cli-config.ts +34 -0
  16. package/cli/configs/tsconfig.cli.json +37 -0
  17. package/cli/configs/tsconfig.server.json +13 -0
  18. package/cli/drizzle.config.ts +6 -12
  19. package/cli/next.config.js +73 -80
  20. package/cli/npm-commands/{gen-meta/index.ts → gen-meta.ts} +3 -3
  21. package/cli/npm-commands/gen-schema.ts +3 -0
  22. package/cli/npm-commands/{migrate-db/migrate-db.ts → migrate-db.ts} +15 -15
  23. package/cli/npm-commands/{seed-db/seed-db.ts → seed-db.ts} +15 -15
  24. package/cli/package-template.json +61 -64
  25. package/cli/playwright.config.ts +6 -34
  26. package/cli/postcss.config.mjs +7 -9
  27. package/cli/server.ts +41 -41
  28. package/cli/src/app/api/v1/[resourceName]/[id]/route.ts +11 -0
  29. package/cli/src/app/api/v1/[resourceName]/route.ts +14 -0
  30. package/cli/src/app/api/v1/[resourceName]/upsert/route.ts +3 -0
  31. package/cli/src/app/api/v1/log/route.ts +7 -0
  32. package/cli/src/app/api/v1/otheruser/[id]/route.ts +14 -0
  33. package/cli/src/app/api/v1/otheruser/route.ts +7 -0
  34. package/cli/src/app/api/v1/password/forgotpassword/route.ts +3 -0
  35. package/cli/src/app/api/v1/password/resetpassword/route.ts +3 -0
  36. package/cli/src/app/api/v1/payment/method/route.ts +4 -0
  37. package/cli/src/app/api/v1/payment/route.ts +3 -0
  38. package/cli/src/app/api/v1/payment/verify-fail/route.ts +3 -0
  39. package/cli/src/app/api/v1/payment/verify-success/route.ts +3 -0
  40. package/cli/src/app/api/v1/preload/route.ts +3 -0
  41. package/cli/src/app/api/v1/searchable-resources/route.ts +11 -0
  42. package/cli/src/app/api/v1/searchresult/route.ts +35 -0
  43. package/cli/src/app/api/v1/sign-in/route.ts +3 -0
  44. package/cli/src/app/api/v1/sign-out/route.ts +3 -0
  45. package/cli/src/app/api/v1/sign-up/route.ts +3 -0
  46. package/cli/src/app/api/v1/subscription/cancel/route.ts +3 -0
  47. package/cli/src/app/api/v1/subscription/create/route.ts +3 -0
  48. package/cli/src/app/api/v1/subscription/update/route.ts +3 -0
  49. package/cli/src/app/api/v1/uimeta/route.ts +3 -0
  50. package/cli/src/app/api/v1/uimetas/route.ts +3 -0
  51. package/cli/src/app/api/v1/userpermission/route.ts +3 -0
  52. package/cli/src/app/api/v1/visible-workspace/route.ts +5 -0
  53. package/cli/src/app/api/v1/workspace/change/route.ts +5 -0
  54. package/cli/src/app/f/(private)/dashboard/page.tsx +8 -8
  55. package/cli/src/app/f/(private)/dynamiclayout/page.tsx +8 -8
  56. package/cli/src/app/f/(private)/edituser/[id]/page.tsx +8 -8
  57. package/cli/src/app/f/(private)/edituser/page.tsx +8 -8
  58. package/cli/src/app/f/(private)/layout.tsx +5 -5
  59. package/cli/src/app/f/(private)/lm/page.tsx +8 -8
  60. package/cli/src/app/f/(private)/payment-plans/page.tsx +8 -8
  61. package/cli/src/app/f/(private)/preload/page.tsx +8 -8
  62. package/cli/src/app/f/(private)/statusboard/page.tsx +8 -8
  63. package/cli/src/app/f/(private)/userlist/page.tsx +8 -8
  64. package/cli/src/app/f/(private)/view/page.tsx +9 -8
  65. package/cli/src/app/f/defaultRedirect/page.tsx +9 -9
  66. package/cli/src/app/f/editpassword/page.tsx +8 -8
  67. package/cli/src/app/f/forgotpassword/page.tsx +13 -13
  68. package/cli/src/app/f/resetpassword/page.tsx +11 -16
  69. package/cli/src/app/f/sign-in/microsoft/page.tsx +8 -8
  70. package/cli/src/app/f/sign-in/page.tsx +13 -13
  71. package/cli/src/app/f/sign-up/page.tsx +13 -13
  72. package/cli/src/app/f/test/feed/page.tsx +8 -8
  73. package/cli/src/app/f/test/file-upload/page.tsx +8 -8
  74. package/cli/src/app/f/test/layout.tsx +5 -5
  75. package/cli/src/app/f/test/page.tsx +8 -8
  76. package/cli/src/app/globals.css +1 -1
  77. package/cli/src/app/layout.tsx +46 -44
  78. package/cli/src/app/page.tsx +9 -9
  79. package/cli/src/app/styles/common.css +71 -74
  80. package/cli/src/app/styles/rich-text-editor.css +130 -130
  81. package/cli/{global-setup.ts → test/global-setup.ts} +25 -25
  82. package/cli/tsconfig.json +28 -65
  83. package/cli/tsconfig.server.json +17 -14
  84. package/fd-toolbox/api/api-client.cjs.js +19 -0
  85. package/fd-toolbox/api/api-client.js +11 -0
  86. package/{level2/fd-toolbox → fd-toolbox}/api/api-path-names.cjs.js +2 -2
  87. package/fd-toolbox/api/api-paths.cjs.js +9 -0
  88. package/fd-toolbox/api/api-paths.js +7 -0
  89. package/fd-toolbox/api/base-api.cjs.js +22 -0
  90. package/fd-toolbox/api/base-api.js +13 -0
  91. package/{level2/fd-toolbox → fd-toolbox}/auth/login-states.cjs.js +1 -1
  92. package/{level2/fd-toolbox → fd-toolbox}/auth/login-states.js +1 -1
  93. package/fd-toolbox/auth/session-storage.cjs.js +12 -0
  94. package/fd-toolbox/auth/session-storage.js +7 -0
  95. package/{level2/fd-toolbox → fd-toolbox}/auth/tokens.cjs.js +1 -1
  96. package/{level2/fd-toolbox → fd-toolbox}/auth/tokens.js +1 -1
  97. package/{level2/fd-toolbox → fd-toolbox}/constants/constants.cjs.js +1 -1
  98. package/fd-toolbox/constants/constants.js +4 -0
  99. package/{level2/fd-toolbox → fd-toolbox}/constants/header-names.cjs.js +1 -1
  100. package/{level2/fd-toolbox → fd-toolbox}/constants/header-names.js +1 -1
  101. package/{level2/fd-toolbox → fd-toolbox}/constants/public-files.cjs.js +2 -1
  102. package/{level2/fd-toolbox → fd-toolbox}/constants/public-files.js +2 -2
  103. package/fd-toolbox/enums/enums.cjs.js +26 -0
  104. package/fd-toolbox/enums/enums.js +4 -0
  105. package/{level2/fd-toolbox → fd-toolbox}/errors/error-handler.cjs.js +1 -1
  106. package/fd-toolbox/errors/error-handler.js +8 -0
  107. package/fd-toolbox/errors/errors.cjs.js +6 -0
  108. package/fd-toolbox/errors/errors.js +4 -0
  109. package/fd-toolbox/errors/problem-details.cjs.js +8 -0
  110. package/fd-toolbox/errors/problem-details.js +6 -0
  111. package/fd-toolbox/functions/value-checking-functions.cjs.js +10 -0
  112. package/fd-toolbox/functions/value-checking-functions.js +6 -0
  113. package/{level2/fd-toolbox → fd-toolbox}/http/url/urls.cjs.js +1 -1
  114. package/fd-toolbox/http/url/urls.js +6 -0
  115. package/fd-toolbox/infra/env-config.cjs.js +8 -0
  116. package/fd-toolbox/infra/env-config.js +6 -0
  117. package/fd-toolbox/infra/env-functions.cjs.js +16 -0
  118. package/fd-toolbox/infra/env-functions.js +11 -0
  119. package/fd-toolbox/infra/env-schema.cjs.js +12 -0
  120. package/fd-toolbox/infra/env-schema.js +8 -0
  121. package/{level2/fd-toolbox → fd-toolbox}/infra/env-store.cjs.js +1 -1
  122. package/{level2/fd-toolbox → fd-toolbox}/infra/env-store.js +1 -1
  123. package/{level2/fd-toolbox → fd-toolbox}/lib/environments.cjs.js +2 -2
  124. package/{level2/fd-toolbox → fd-toolbox}/lib/environments.js +2 -2
  125. package/fd-toolbox/lib/utils.cjs.js +29 -0
  126. package/fd-toolbox/lib/utils.js +13 -0
  127. package/fd-toolbox/local-storage/local-storage.cjs.js +12 -0
  128. package/fd-toolbox/local-storage/local-storage.js +7 -0
  129. package/fd-toolbox/logging/loggers.cjs.js +16 -0
  130. package/fd-toolbox/logging/loggers.js +10 -0
  131. package/fd-toolbox/notifications.cjs.js +14 -0
  132. package/fd-toolbox/notifications.js +7 -0
  133. package/{level2/fd-toolbox → fd-toolbox}/odata/odata-constants.cjs.js +1 -1
  134. package/fd-toolbox/odata/odata-constants.js +4 -0
  135. package/fd-toolbox/odata/odatas.cjs.js +11 -0
  136. package/fd-toolbox/odata/odatas.js +9 -0
  137. package/fd-toolbox/odata/services/odata-filters.cjs.js +11 -0
  138. package/fd-toolbox/odata/services/odata-filters.js +9 -0
  139. package/fd-toolbox/paths/paths-names.cjs.js +9 -0
  140. package/fd-toolbox/paths/paths-names.js +6 -0
  141. package/{level2/fd-toolbox → fd-toolbox}/routing/login-routers.cjs.js +1 -1
  142. package/{level2/fd-toolbox → fd-toolbox}/routing/login-routers.js +1 -1
  143. package/fd-toolbox/routing/paths.cjs.js +8 -0
  144. package/fd-toolbox/routing/paths.js +6 -0
  145. package/{level2/fd-toolbox → fd-toolbox}/strings/strings-constants.cjs.js +1 -1
  146. package/fd-toolbox/strings/strings-constants.js +4 -0
  147. package/fd-toolbox/strings/strings.cjs.js +10 -0
  148. package/fd-toolbox/strings/strings.js +7 -0
  149. package/fd-toolbox/types/ensure-type.cjs.js +21 -0
  150. package/fd-toolbox/types/ensure-type.js +4 -0
  151. package/fd-toolbox-core/core/name-of.cjs.js +8 -0
  152. package/fd-toolbox-core/core/name-of.js +4 -0
  153. package/fd-toolbox-core/types/resource-with-id.cjs.js +9 -0
  154. package/fd-toolbox-core/types/resource-with-id.js +6 -0
  155. package/level2/cli/bin/index.cjs.js +8 -0
  156. package/level2/cli/bin/index.js +6 -0
  157. package/level2/cli/create/bin/create.cjs.js +42 -0
  158. package/level2/cli/create/bin/create.js +20 -0
  159. package/level2/npm-commands/build-npm/cli-contents.cjs.js +9 -0
  160. package/level2/npm-commands/build-npm/cli-contents.js +6 -0
  161. package/level2/npm-commands/build-npm/path.cjs.js +1 -1
  162. package/level2/npm-commands/build-npm/path.js +2 -2
  163. package/package.json +31 -30
  164. package/cli/.gitlab-ci.yml +0 -16
  165. package/cli/.husky/commit-msg +0 -1
  166. package/cli/.husky/pre-commit +0 -1
  167. package/cli/eslint.config.js +0 -10
  168. package/cli/npm-commands/gen-schema/index.ts +0 -5
  169. package/cli/npm-commands/migrate-db/index.ts +0 -5
  170. package/cli/npm-commands/seed-db/index.ts +0 -5
  171. package/cli/tailwind.config.ts +0 -13
  172. package/cli/tsconfig.declaration.json +0 -48
  173. package/level2/cli/create-saas-kit-app/bin/index.cjs.js +0 -40
  174. package/level2/cli/create-saas-kit-app/bin/index.js +0 -18
  175. package/level2/fd-toolbox/api/api-client.cjs.js +0 -19
  176. package/level2/fd-toolbox/api/api-client.js +0 -11
  177. package/level2/fd-toolbox/api/api-paths.cjs.js +0 -9
  178. package/level2/fd-toolbox/api/api-paths.js +0 -7
  179. package/level2/fd-toolbox/api/base-api.cjs.js +0 -22
  180. package/level2/fd-toolbox/api/base-api.js +0 -13
  181. package/level2/fd-toolbox/auth/session-storage.cjs.js +0 -12
  182. package/level2/fd-toolbox/auth/session-storage.js +0 -7
  183. package/level2/fd-toolbox/constants/constants.js +0 -4
  184. package/level2/fd-toolbox/enums/enums.cjs.js +0 -26
  185. package/level2/fd-toolbox/enums/enums.js +0 -4
  186. package/level2/fd-toolbox/errors/error-handler.js +0 -8
  187. package/level2/fd-toolbox/errors/errors.cjs.js +0 -6
  188. package/level2/fd-toolbox/errors/errors.js +0 -4
  189. package/level2/fd-toolbox/errors/problem-details.cjs.js +0 -8
  190. package/level2/fd-toolbox/errors/problem-details.js +0 -6
  191. package/level2/fd-toolbox/functions/value-checking-functions.cjs.js +0 -10
  192. package/level2/fd-toolbox/functions/value-checking-functions.js +0 -6
  193. package/level2/fd-toolbox/http/url/urls.js +0 -6
  194. package/level2/fd-toolbox/infra/env-config.cjs.js +0 -8
  195. package/level2/fd-toolbox/infra/env-config.js +0 -6
  196. package/level2/fd-toolbox/infra/env-functions.cjs.js +0 -16
  197. package/level2/fd-toolbox/infra/env-functions.js +0 -11
  198. package/level2/fd-toolbox/infra/env-schema.cjs.js +0 -12
  199. package/level2/fd-toolbox/infra/env-schema.js +0 -8
  200. package/level2/fd-toolbox/lib/utils.cjs.js +0 -29
  201. package/level2/fd-toolbox/lib/utils.js +0 -13
  202. package/level2/fd-toolbox/local-storage/local-storage.cjs.js +0 -12
  203. package/level2/fd-toolbox/local-storage/local-storage.js +0 -7
  204. package/level2/fd-toolbox/logging/loggers.cjs.js +0 -16
  205. package/level2/fd-toolbox/logging/loggers.js +0 -10
  206. package/level2/fd-toolbox/notifications.cjs.js +0 -14
  207. package/level2/fd-toolbox/notifications.js +0 -7
  208. package/level2/fd-toolbox/odata/odata-constants.js +0 -4
  209. package/level2/fd-toolbox/odata/odatas.cjs.js +0 -11
  210. package/level2/fd-toolbox/odata/odatas.js +0 -9
  211. package/level2/fd-toolbox/odata/services/odata-filters.cjs.js +0 -11
  212. package/level2/fd-toolbox/odata/services/odata-filters.js +0 -9
  213. package/level2/fd-toolbox/paths/paths-names.cjs.js +0 -9
  214. package/level2/fd-toolbox/paths/paths-names.js +0 -6
  215. package/level2/fd-toolbox/routing/paths.cjs.js +0 -8
  216. package/level2/fd-toolbox/routing/paths.js +0 -6
  217. package/level2/fd-toolbox/strings/strings-constants.js +0 -4
  218. package/level2/fd-toolbox/strings/strings.cjs.js +0 -10
  219. package/level2/fd-toolbox/strings/strings.js +0 -7
  220. package/level2/fd-toolbox/types/ensure-type.cjs.js +0 -18
  221. package/level2/fd-toolbox/types/ensure-type.js +0 -4
  222. package/level2/fd-toolbox-core/core/name-of.cjs.js +0 -8
  223. package/level2/fd-toolbox-core/core/name-of.js +0 -4
  224. package/level2/fd-toolbox-core/types/resource-with-id.cjs.js +0 -9
  225. package/level2/fd-toolbox-core/types/resource-with-id.js +0 -6
  226. /package/{level2/fd-toolbox → fd-toolbox}/api/api-path-names.js +0 -0
  227. /package/{level2/fd-toolbox → fd-toolbox}/constants/api-constants.cjs.js +0 -0
  228. /package/{level2/fd-toolbox → fd-toolbox}/constants/api-constants.js +0 -0
  229. /package/{level2/fd-toolbox → fd-toolbox}/constants/environment-constants.cjs.js +0 -0
  230. /package/{level2/fd-toolbox → fd-toolbox}/constants/environment-constants.js +0 -0
  231. /package/{level2/fd-toolbox → fd-toolbox}/constants/extensions.cjs.js +0 -0
  232. /package/{level2/fd-toolbox → fd-toolbox}/constants/extensions.js +0 -0
  233. /package/{level2/fd-toolbox → fd-toolbox}/constants/http-status-codes.cjs.js +0 -0
  234. /package/{level2/fd-toolbox → fd-toolbox}/constants/http-status-codes.js +0 -0
  235. /package/{level2/fd-toolbox → fd-toolbox}/constants/meta-query-params.cjs.js +0 -0
  236. /package/{level2/fd-toolbox → fd-toolbox}/constants/meta-query-params.js +0 -0
  237. /package/{level2/fd-toolbox → fd-toolbox}/constants/representations.cjs.js +0 -0
  238. /package/{level2/fd-toolbox → fd-toolbox}/constants/representations.js +0 -0
  239. /package/{level2/fd-toolbox → fd-toolbox}/errors/error-statuses.cjs.js +0 -0
  240. /package/{level2/fd-toolbox → fd-toolbox}/errors/error-statuses.js +0 -0
  241. /package/{level2/fd-toolbox → fd-toolbox}/http/http-constants.cjs.js +0 -0
  242. /package/{level2/fd-toolbox → fd-toolbox}/http/http-constants.js +0 -0
  243. /package/{level2/fd-toolbox → fd-toolbox}/infra/env-setting-types.cjs.js +0 -0
  244. /package/{level2/fd-toolbox → fd-toolbox}/infra/env-setting-types.js +0 -0
  245. /package/{level2/fd-toolbox → fd-toolbox}/infra/toolbox-env-setting-keys.cjs.js +0 -0
  246. /package/{level2/fd-toolbox → fd-toolbox}/infra/toolbox-env-setting-keys.js +0 -0
  247. /package/{level2/fd-toolbox → fd-toolbox}/logging/logging-constants.cjs.js +0 -0
  248. /package/{level2/fd-toolbox → fd-toolbox}/logging/logging-constants.js +0 -0
  249. /package/{level2/fd-toolbox → fd-toolbox}/odata/odata-enums.cjs.js +0 -0
  250. /package/{level2/fd-toolbox → fd-toolbox}/odata/odata-enums.js +0 -0
  251. /package/{level2/fd-toolbox → fd-toolbox}/resources/resource-names.cjs.js +0 -0
  252. /package/{level2/fd-toolbox → fd-toolbox}/resources/resource-names.js +0 -0
  253. /package/{level2/fd-toolbox → fd-toolbox}/routing/routes.cjs.js +0 -0
  254. /package/{level2/fd-toolbox → fd-toolbox}/routing/routes.js +0 -0
  255. /package/{level2/fd-toolbox-core → fd-toolbox-core}/constants/meta-constants.cjs.js +0 -0
  256. /package/{level2/fd-toolbox-core → fd-toolbox-core}/constants/meta-constants.js +0 -0
  257. /package/{level2/fd-toolbox-core → fd-toolbox-core}/constants/promises.cjs.js +0 -0
  258. /package/{level2/fd-toolbox-core → fd-toolbox-core}/constants/promises.js +0 -0
  259. /package/{level2/fd-toolbox-core → fd-toolbox-core}/enums/log-severities.cjs.js +0 -0
  260. /package/{level2/fd-toolbox-core → fd-toolbox-core}/enums/log-severities.js +0 -0
@@ -1,383 +1,444 @@
1
- #!/usr/bin/env node
2
-
3
- import { execSync } from "child_process";
4
- import { promises as fsPromises, existsSync, mkdirSync, rmSync, Stats } from "fs";
5
- import { fileURLToPath } from "node:url";
6
- import * as path from "path";
7
- import fs from "node:fs";
8
- import {
9
- toolsFoldersToReplace,
10
- additionalFoldersToCreate,
11
- } from "@packages/base-repo/constants/create-app-constants";
12
-
13
- import { logStringError, logInfoRaw } from "@packages/level2/fd-toolbox/logging/loggers";
14
-
15
- import { WithIndexer } from "@packages/level2/fd-toolbox-core/types/with-indexer";
16
-
17
- import { projectName } from "@packages/level2/fd-toolbox/constants/constants";
18
-
19
- import { allFilesToCopy } from "@packages/level2/npm-commands/build-npm/path";
20
-
21
- import { createError } from "@packages/level2/fd-toolbox/errors/errors";
22
-
23
- import { files, folders } from "@packages/base-repo/app-constants/project-paths";
24
-
25
- import { npmPackages } from "@packages/base-repo/constants/packages";
26
-
27
- import { aliasSymbols } from "@packages/base-repo/app-constants/aliases";
28
-
29
- import { isString, isWithIndexer } from "@packages/level2/fd-toolbox/types/ensure-type";
30
-
31
- interface PackageJson {
32
- name?: string;
33
- dependencies?: WithIndexer<string>;
34
- devDependencies?: WithIndexer<string>;
35
- [key: string]: unknown;
36
- }
37
-
38
- interface FileEntry {
39
- name: string;
40
- isDirectory(): boolean;
41
- }
42
-
43
- const requiredNodeVersion = "v18.0.0";
44
-
45
- if (process.version < requiredNodeVersion) {
46
- logStringError(`Node.js ${requiredNodeVersion}+ is required`);
47
- process.exit(1);
48
- }
49
-
50
- const filename = fileURLToPath(import.meta.url);
51
- const sourceDir = "../../../../";
52
- const binDir = path.dirname(filename);
53
- const repoRoot = path.resolve(binDir, sourceDir);
54
- const envLocalPath: string = path.join(binDir, `${sourceDir}${folders.cli}/${files.env}`);
55
- const envTemplate: string = fs.readFileSync(envLocalPath, "utf8");
56
-
57
- const foldersToIgnore: string[] = [`${folders.cli}/constants`];
58
-
59
- function runCommand(command: string, cwd?: string) {
60
- logInfoRaw(`Executing: ${command}`);
61
-
62
- try {
63
- execSync(command, { stdio: "inherit", cwd: cwd ?? process.cwd() });
64
- } catch (error) {
65
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
66
- logStringError(`Error executing: ${command}`);
67
- logStringError(errorMessage);
68
- process.exit(1);
69
- }
70
- }
71
-
72
- function checkTargetFolder(targetDir: string) {
73
- if (existsSync(targetDir)) {
74
- logStringError(`Error: Folder "${path.basename(targetDir)}" already exists.`);
75
- process.exit(1);
76
- }
77
- }
78
-
79
- function getCreateNextAppVersion() {
80
- const packageJsonPath: string = path.resolve(repoRoot, "package.json");
81
- const packageJson: PackageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
82
-
83
- return String(packageJson.devDependencies?.["next"] ?? packageJson.dependencies?.["next"] ?? "latest");
84
- }
85
-
86
- function createNextProject(projectTitle: string, targetDir: string) {
87
- logInfoRaw("Creating Next.js project...");
88
-
89
- const createNextAppVersion = getCreateNextAppVersion();
90
- const createCommand: string = [
91
- `npx create-next-app@${createNextAppVersion} "${projectTitle}"`,
92
- "--typescript",
93
- "--tailwind",
94
- "--eslint",
95
- "--app",
96
- "--src-dir",
97
- "--turbopack",
98
- "--no-import-alias",
99
- "--no-git",
100
- ].join(" ");
101
-
102
- runCommand(createCommand, path.dirname(targetDir));
103
- logInfoRaw("Next.js project created!");
104
-
105
- const tsConfigPath = path.join(targetDir, "next.config.ts");
106
-
107
- if (fs.existsSync(tsConfigPath)) {
108
- fs.rmSync(tsConfigPath);
109
- logInfoRaw("Removed default next.config.ts");
110
- }
111
- }
112
-
113
- async function createEnvFile(targetDir: string) {
114
- logInfoRaw("Creating .env file with default values...");
115
-
116
- const envPath: string = path.join(targetDir, ".env");
117
-
118
- try {
119
- await fsPromises.writeFile(envPath, envTemplate, "utf8");
120
- logInfoRaw(".env file created with values!");
121
- } catch (error) {
122
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
123
- logStringError("Error creating .env file:");
124
- logStringError(errorMessage);
125
- throw error;
126
- }
127
- }
128
-
129
- async function overridePackageJson(targetDir: string) {
130
- const targetPackageJsonPath: string = path.join(targetDir, "package.json");
131
- // "name" is omitted in package.json; renamed to package-template.json so NX ignores it as a project.
132
- const cliPackageJsonPath: string = path.join(repoRoot, `${folders.cli}/package-template.json`);
133
-
134
- try {
135
- const targetPackageJson: PackageJson = JSON.parse(
136
- await fsPromises.readFile(targetPackageJsonPath, "utf8"),
137
- );
138
- const name = targetPackageJson.name;
139
-
140
- const cliPackageJson: PackageJson = JSON.parse(await fsPromises.readFile(cliPackageJsonPath, "utf8"));
141
-
142
- const finalPackageJson: PackageJson = {
143
- ...cliPackageJson,
144
- name: name,
145
- };
146
-
147
- await fsPromises.writeFile(
148
- targetPackageJsonPath,
149
- `${JSON.stringify(finalPackageJson, null, 2)}\n`,
150
- "utf8",
151
- );
152
- } catch (error) {
153
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
154
- logStringError("Error overriding package.json:");
155
- logStringError(errorMessage);
156
- throw error;
157
- }
158
- }
159
-
160
- function replaceImports(content: string) {
161
- // Approved
162
- // eslint-disable-next-line sonarjs/slow-regex
163
- const importRegex = /(import\s+)([^'"]+)(\s+from\s+["'])([^"']+)(["'])/g;
164
-
165
- return content.replace(importRegex, (match, p1, importItems, p3, importPath, p5) => {
166
- if (!importPath.startsWith(aliasSymbols.atSign) || importPath.startsWith(npmPackages.saasKit)) {
167
- return match;
168
- }
169
-
170
- let newImportPath;
171
-
172
- if (
173
- importPath.startsWith(aliasSymbols.atSign + folders.server) ||
174
- importPath.includes(`/${folders.server}/`)
175
- ) {
176
- newImportPath = npmPackages.saasKitServer;
177
- } else {
178
- newImportPath = npmPackages.saasKit;
179
- }
180
-
181
- let newImportItems = importItems.trim();
182
-
183
- if (!newImportItems.startsWith("{")) {
184
- newImportItems = `{ ${newImportItems} }`;
185
- }
186
-
187
- return `${p1}${newImportItems}${p3}${newImportPath}${p5}`;
188
- });
189
- }
190
-
191
- async function copyProjectFiles(targetDir: string) {
192
- logInfoRaw(`Copying ${projectName} files...`);
193
-
194
- for (const file of allFilesToCopy) {
195
- const cliFile = `${folders.cli}/${file}`;
196
- const srcPath: string = path.join(repoRoot, cliFile);
197
-
198
- const processedFile = replaceToolsFolder(file);
199
-
200
- const destPath: string = path.join(targetDir, processedFile);
201
-
202
- if (existsSync(srcPath)) {
203
- const stats: Stats = await fsPromises.stat(srcPath);
204
-
205
- if (stats.isDirectory()) {
206
- await copyDirectoryContents(srcPath, destPath);
207
- } else {
208
- await copyAndProcessFile(srcPath, destPath);
209
- }
210
- } else {
211
- logInfoRaw(`Source file not found: ${cliFile}`);
212
- }
213
- }
214
-
215
- logInfoRaw("Files copied and imports updated!");
216
- }
217
-
218
- function replaceToolsFolder(file: string) {
219
- let processedFile = file;
220
-
221
- for (const [folderPrefix, replacements] of Object.entries(toolsFoldersToReplace)) {
222
- if (processedFile.startsWith(folderPrefix) && isWithIndexer<string>(replacements)) {
223
- const typedReplacements: WithIndexer<string> = replacements;
224
-
225
- for (const key in typedReplacements) {
226
- const value = typedReplacements[key];
227
-
228
- if (isString(value)) {
229
- processedFile = processedFile.replace(key, value);
230
- }
231
- }
232
-
233
- break;
234
- }
235
- }
236
-
237
- return processedFile;
238
- }
239
-
240
- function shouldIgnorePath(filePath: string, basePath: string, isDirectory = false) {
241
- const relativePath = path.relative(basePath, filePath);
242
- const pathParts = relativePath.split(path.sep);
243
-
244
- const relativeFromRepo = path.relative(repoRoot, filePath).replace(/\\/g, "/");
245
-
246
- return (
247
- (isDirectory && foldersToIgnore.includes(relativeFromRepo)) ||
248
- foldersToIgnore.some((ignoreFolder) => pathParts.some((part) => part === ignoreFolder))
249
- );
250
- }
251
-
252
- async function copyDirectoryContents(srcDir: string, destDir: string) {
253
- if (!existsSync(destDir)) {
254
- mkdirSync(destDir, { recursive: true });
255
- }
256
-
257
- try {
258
- const entries: FileEntry[] = await fsPromises.readdir(srcDir, { withFileTypes: true });
259
-
260
- for (const entry of entries) {
261
- const srcPath: string = path.join(srcDir, entry.name);
262
- const destPath: string = path.join(destDir, entry.name);
263
-
264
- if (!shouldIgnorePath(srcPath, srcDir, entry.isDirectory())) {
265
- if (entry.isDirectory()) {
266
- await copyDirectoryContents(srcPath, destPath);
267
- } else {
268
- await copyAndProcessFile(srcPath, destPath);
269
- }
270
- }
271
- }
272
- } catch (error) {
273
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
274
- logStringError(`Error reading directory ${srcDir}: ${errorMessage}`);
275
- throw error;
276
- }
277
- }
278
-
279
- async function copyAndProcessFile(srcPath: string, destPath: string) {
280
- const destDir: string = path.dirname(destPath);
281
-
282
- if (!existsSync(destDir)) {
283
- mkdirSync(destDir, { recursive: true });
284
- }
285
-
286
- const stats: Stats = await fsPromises.stat(srcPath);
287
-
288
- if (stats.isDirectory()) {
289
- await copyDirectoryContents(srcPath, destPath);
290
- } else {
291
- const ext = path.extname(srcPath).toLowerCase();
292
-
293
- const binaryExtensions = [".ico", ".png", ".jpg"];
294
-
295
- if (binaryExtensions.includes(ext)) {
296
- await fsPromises.copyFile(srcPath, destPath);
297
- return;
298
- }
299
-
300
- const content = await fsPromises.readFile(srcPath, "utf8");
301
- const processedContent = replaceImports(content);
302
- await fsPromises.writeFile(destPath, processedContent, "utf8");
303
- }
304
- }
305
-
306
- function createAdditionalFolders(targetDir: string) {
307
- for (const folder of additionalFoldersToCreate) {
308
- const folderPath: string = path.join(targetDir, folder);
309
-
310
- if (!existsSync(folderPath)) {
311
- mkdirSync(folderPath, { recursive: true });
312
- logInfoRaw(`Created folder: ${folder}`);
313
- }
314
- }
315
-
316
- logInfoRaw("Additional folders created!");
317
- }
318
-
319
- async function main() {
320
- const args: string[] = process.argv.slice(2);
321
-
322
- if (args.length < 1) {
323
- logStringError("Error: Specify folder name. Example: npx create-saas-kit-app <folder-name>");
324
- process.exit(1);
325
- }
326
-
327
- const folderName: string = args[0];
328
- const targetDir: string = path.resolve(process.cwd(), folderName);
329
-
330
- logInfoRaw(`\nCreating ${projectName} project "${folderName}"...\n`);
331
-
332
- try {
333
- checkTargetFolder(targetDir);
334
- createNextProject(folderName, targetDir);
335
- await copyProjectFiles(targetDir);
336
- await overridePackageJson(targetDir);
337
- createAdditionalFolders(targetDir);
338
- await createEnvFile(targetDir);
339
- await updateLayoutTitle(targetDir, folderName);
340
-
341
- logInfoRaw(`\n${projectName} project "${folderName}" is ready!`);
342
- logInfoRaw("Next steps:");
343
- logInfoRaw(` cd ${folderName}`);
344
- logInfoRaw(" npm run prod");
345
- logInfoRaw(" npm start");
346
- } catch (error) {
347
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
348
- logStringError("Error:");
349
- logStringError(errorMessage);
350
-
351
- if (existsSync(targetDir)) {
352
- logInfoRaw("Cleaning up...");
353
- rmSync(targetDir, { recursive: true, force: true });
354
- }
355
-
356
- process.exit(1);
357
- }
358
- }
359
-
360
- async function updateLayoutTitle(targetDir: string, folderName: string) {
361
- const layoutPath = path.join(targetDir, "src/app/layout.tsx");
362
-
363
- if (!existsSync(layoutPath)) {
364
- throw createError(`layout.tsx not found at ${layoutPath}, cannot update title.`);
365
- }
366
-
367
- let content = await fsPromises.readFile(layoutPath, "utf8");
368
- const titleRegex = /"SaaS Kit"/g;
369
-
370
- if (!titleRegex.test(content)) {
371
- throw createError("SaaS Kit not found in layout.tsx, cannot update title.");
372
- }
373
-
374
- content = content.replace(titleRegex, `"${folderName}"`);
375
- await fsPromises.writeFile(layoutPath, content, "utf8");
376
- }
377
-
378
- main().catch((error) => {
379
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
380
- logStringError("Unhandled error:");
381
- logStringError(errorMessage);
382
- process.exit(1);
383
- });
1
+ import { execSync } from "child_process";
2
+ import { promises as fsPromises, existsSync, mkdirSync, rmSync, Stats } from "fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import * as path from "path";
5
+ import fs from "node:fs";
6
+ import {
7
+ toolsFoldersToReplace,
8
+ additionalFoldersToCreate,
9
+ } from "@packages/base-repo/constants/create-app-constants";
10
+
11
+ import { logStringError, logInfoRaw } from "@packages/fd-toolbox/logging/loggers";
12
+
13
+ import { WithIndexer } from "@packages/fd-toolbox-core/types/with-indexer";
14
+
15
+ import { projectName } from "@packages/fd-toolbox/constants/constants";
16
+
17
+ import { allFilesToCopy } from "@packages/level2/npm-commands/build-npm/path";
18
+
19
+ import { createError } from "@packages/fd-toolbox/errors/errors";
20
+
21
+ import { files, folders, projectPaths } from "@packages/base-repo/app-constants/project-paths";
22
+
23
+ import { npmPackages } from "@packages/base-repo/constants/packages";
24
+
25
+ import { aliasSymbols } from "@packages/base-repo/app-constants/aliases";
26
+
27
+ import { isString, isWithIndexer } from "@packages/fd-toolbox/types/ensure-type";
28
+
29
+ import { cliEslintContent, cliScripts } from "@npm-commands/build-npm/cli-contents";
30
+
31
+ interface PackageJson {
32
+ name?: string;
33
+ dependencies?: WithIndexer<string>;
34
+ devDependencies?: WithIndexer<string>;
35
+ [key: string]: unknown;
36
+ }
37
+
38
+ interface FileEntry {
39
+ name: string;
40
+ isDirectory(): boolean;
41
+ }
42
+
43
+ const requiredNodeVersion = "v20.0.0";
44
+
45
+ if (process.version < requiredNodeVersion) {
46
+ logStringError(`Node.js ${requiredNodeVersion}+ is required`);
47
+ process.exit(1);
48
+ }
49
+
50
+ const filename = fileURLToPath(import.meta.url);
51
+ const sourceDir = "../../../../";
52
+ const binDir = path.dirname(filename);
53
+ const repoRoot = path.resolve(binDir, sourceDir);
54
+ const envLocalPath: string = path.join(binDir, `${sourceDir}${folders.cli}${path.sep}${files.envExample}`);
55
+ const envTemplate: string = fs.readFileSync(envLocalPath, "utf8");
56
+
57
+ const foldersToIgnore: string[] = [
58
+ `${folders.cli}${path.sep}${folders.constants}`,
59
+ `${folders.cli}${path.sep}${files.packageTemplate}`,
60
+ `${folders.cli}${path.sep}${files.envExample}`,
61
+ `${folders.cli}${path.sep}${folders.bin}`,
62
+ `${folders.cli}${path.sep}${folders.configs}`,
63
+ ];
64
+
65
+ function runCommand(command: string, cwd?: string) {
66
+ logInfoRaw(`Executing: ${command}`);
67
+
68
+ try {
69
+ execSync(command, { stdio: "inherit", cwd: cwd ?? process.cwd() });
70
+ } catch (error) {
71
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
72
+ logStringError(`Error executing: ${command}`);
73
+ logStringError(errorMessage);
74
+ process.exit(1);
75
+ }
76
+ }
77
+
78
+ function checkTargetFolder(targetDir: string) {
79
+ if (existsSync(targetDir)) {
80
+ logStringError(`Error: Folder "${path.basename(targetDir)}" already exists.`);
81
+ process.exit(1);
82
+ }
83
+ }
84
+
85
+ function getCreateNextAppVersion() {
86
+ const packageJsonPath: string = path.resolve(repoRoot, files.packageJson);
87
+ const packageJson: PackageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
88
+
89
+ return String(packageJson.devDependencies?.["next"] ?? packageJson.dependencies?.["next"] ?? "latest");
90
+ }
91
+
92
+ function createNextProject(projectTitle: string, targetDir: string) {
93
+ logInfoRaw("Creating Next.js project...");
94
+
95
+ const createNextAppVersion = getCreateNextAppVersion();
96
+ const createCommand: string = [
97
+ `npx --yes create-next-app@${createNextAppVersion} "${projectTitle}"`,
98
+ "--typescript",
99
+ "--tailwind",
100
+ "--eslint",
101
+ "--app",
102
+ "--src-dir",
103
+ "--turbopack",
104
+ "--no-import-alias",
105
+ "--no-git",
106
+ "--react-compiler",
107
+ ].join(" ");
108
+
109
+ runCommand(createCommand, path.dirname(targetDir));
110
+ logInfoRaw("Next.js project created!");
111
+
112
+ const tsConfigPath = path.join(targetDir, "next.config.ts");
113
+
114
+ if (fs.existsSync(tsConfigPath)) {
115
+ fs.rmSync(tsConfigPath);
116
+ logInfoRaw("Removed default next.config.ts");
117
+ }
118
+ }
119
+
120
+ async function createEnvFile(targetDir: string) {
121
+ logInfoRaw(`Creating ${files.env} file with default values...`);
122
+
123
+ const envPath: string = path.join(targetDir, files.env);
124
+
125
+ try {
126
+ await fsPromises.writeFile(envPath, envTemplate, "utf8");
127
+ logInfoRaw(`${files.env} file created with values!`);
128
+ } catch (error) {
129
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
130
+ logStringError(`Error creating ${files.env} file:`);
131
+ logStringError(errorMessage);
132
+ throw error;
133
+ }
134
+ }
135
+
136
+ async function overridePackageJson(targetDir: string) {
137
+ const targetPackageJsonPath: string = path.join(targetDir, files.packageJson);
138
+ // "name" is omitted in package.json; renamed to package-template.json so NX ignores it as a project.
139
+ const cliPackageJsonPath: string = path.join(
140
+ repoRoot,
141
+ `${folders.cli}${path.sep}${files.packageTemplate}`,
142
+ );
143
+
144
+ try {
145
+ const targetPackageJson: PackageJson = JSON.parse(
146
+ await fsPromises.readFile(targetPackageJsonPath, "utf8"),
147
+ );
148
+ const name = targetPackageJson.name;
149
+
150
+ const cliPackageJson: PackageJson = JSON.parse(await fsPromises.readFile(cliPackageJsonPath, "utf8"));
151
+
152
+ const finalPackageJson: PackageJson = {
153
+ ...cliPackageJson,
154
+ name: name,
155
+ };
156
+
157
+ await fsPromises.writeFile(
158
+ targetPackageJsonPath,
159
+ `${JSON.stringify(finalPackageJson, null, 2)}\n`,
160
+ "utf8",
161
+ );
162
+ } catch (error) {
163
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
164
+ logStringError(`Error overriding ${files.packageJson}:`);
165
+ logStringError(errorMessage);
166
+ throw error;
167
+ }
168
+ }
169
+
170
+ function replaceImports(content: string) {
171
+ const importRegex = /(import\s+)([^'"]+)(\s+from\s+["'])([^"']+)(["'])/g;
172
+
173
+ return content.replace(importRegex, (match, p1, importItems, p3, importPath, p5) => {
174
+ if (!importPath.startsWith(aliasSymbols.atSign) || importPath.startsWith(npmPackages.saasKit)) {
175
+ return match;
176
+ }
177
+
178
+ let newImportPath;
179
+
180
+ if (
181
+ importPath.startsWith(aliasSymbols.atSign + folders.server) ||
182
+ importPath.includes(`${projectPaths.server}`)
183
+ ) {
184
+ newImportPath = npmPackages.saasKitServer;
185
+ } else {
186
+ newImportPath = npmPackages.saasKit;
187
+ }
188
+
189
+ let newImportItems = importItems.trim();
190
+
191
+ const isTypeImport = /^type\b/.test(newImportItems);
192
+ const isNamespaceImport = /^\*\s+as\s+/.test(newImportItems);
193
+
194
+ if (!isTypeImport && !isNamespaceImport && !newImportItems.startsWith("{")) {
195
+ newImportItems = `{ ${newImportItems} }`;
196
+ }
197
+
198
+ return `${p1}${newImportItems}${p3}${newImportPath}${p5}`;
199
+ });
200
+ }
201
+
202
+ async function copyProjectFiles(targetDir: string) {
203
+ logInfoRaw(`Copying ${projectName} files...`);
204
+
205
+ for (const file of allFilesToCopy) {
206
+ const cliFile = `${folders.cli}${path.sep}${file}`;
207
+ const srcPath: string = path.join(repoRoot, cliFile);
208
+
209
+ const processedFile = replaceToolsFolder(file);
210
+
211
+ const destPath: string = path.join(targetDir, processedFile);
212
+
213
+ if (existsSync(srcPath)) {
214
+ const stats: Stats = await fsPromises.stat(srcPath);
215
+
216
+ if (stats.isDirectory()) {
217
+ await copyDirectoryContents(srcPath, destPath);
218
+ } else {
219
+ await copyAndProcessFile(srcPath, destPath);
220
+ }
221
+ } else {
222
+ logInfoRaw(`Source file not found: ${cliFile}`);
223
+ }
224
+ }
225
+
226
+ logInfoRaw("Files copied and imports updated!");
227
+ }
228
+
229
+ function replaceToolsFolder(file: string) {
230
+ let processedFile = file;
231
+
232
+ for (const [folderPrefix, replacements] of Object.entries(toolsFoldersToReplace)) {
233
+ if (processedFile.startsWith(folderPrefix) && isWithIndexer<string>(replacements)) {
234
+ const typedReplacements: WithIndexer<string> = replacements;
235
+
236
+ for (const key in typedReplacements) {
237
+ const value = typedReplacements[key];
238
+
239
+ if (isString(value)) {
240
+ processedFile = processedFile.replace(key, value);
241
+ }
242
+ }
243
+
244
+ break;
245
+ }
246
+ }
247
+
248
+ return processedFile;
249
+ }
250
+
251
+ function shouldIgnorePath(filePath: string, basePath: string, isDirectory = false) {
252
+ const relativeFromRepo = path.relative(repoRoot, filePath).replace(/\\/g, "/");
253
+ const normalizedFoldersToIgnore = foldersToIgnore.map((p) => p.replace(/\\/g, "/"));
254
+
255
+ if (normalizedFoldersToIgnore.includes(relativeFromRepo)) {
256
+ return true;
257
+ }
258
+
259
+ if (isDirectory) {
260
+ const relativePath = path.relative(basePath, filePath).replace(/\\/g, "/");
261
+ const pathParts = relativePath.split("/");
262
+ return normalizedFoldersToIgnore.some((ignoreFolder) =>
263
+ pathParts.some((part) => part === ignoreFolder),
264
+ );
265
+ }
266
+
267
+ return false;
268
+ }
269
+
270
+ async function copyDirectoryContents(srcDir: string, destDir: string) {
271
+ if (!existsSync(destDir)) {
272
+ mkdirSync(destDir, { recursive: true });
273
+ }
274
+
275
+ try {
276
+ const entries: FileEntry[] = await fsPromises.readdir(srcDir, { withFileTypes: true });
277
+
278
+ for (const entry of entries) {
279
+ const srcPath: string = path.join(srcDir, entry.name);
280
+ const destPath: string = path.join(destDir, entry.name);
281
+
282
+ if (!shouldIgnorePath(srcPath, srcDir, entry.isDirectory())) {
283
+ if (entry.isDirectory()) {
284
+ await copyDirectoryContents(srcPath, destPath);
285
+ } else {
286
+ await copyAndProcessFile(srcPath, destPath);
287
+ }
288
+ }
289
+ }
290
+ } catch (error) {
291
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
292
+ logStringError(`Error reading directory ${srcDir}: ${errorMessage}`);
293
+ throw error;
294
+ }
295
+ }
296
+
297
+ async function copyAndProcessFile(srcPath: string, destPath: string) {
298
+ const destDir: string = path.dirname(destPath);
299
+
300
+ if (!existsSync(destDir)) {
301
+ mkdirSync(destDir, { recursive: true });
302
+ }
303
+
304
+ const stats: Stats = await fsPromises.stat(srcPath);
305
+
306
+ if (stats.isDirectory()) {
307
+ await copyDirectoryContents(srcPath, destPath);
308
+ } else {
309
+ const ext = path.extname(srcPath).toLowerCase();
310
+
311
+ const binaryExtensions = [".ico", ".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".pdf"];
312
+
313
+ if (binaryExtensions.includes(ext)) {
314
+ await fsPromises.copyFile(srcPath, destPath);
315
+ return;
316
+ }
317
+
318
+ const content = await fsPromises.readFile(srcPath, "utf8");
319
+ const processedContent = replaceImports(content);
320
+ await fsPromises.writeFile(destPath, processedContent, "utf8");
321
+ }
322
+ }
323
+
324
+ function createAdditionalFolders(targetDir: string) {
325
+ for (const folder of additionalFoldersToCreate) {
326
+ const folderPath: string = path.join(targetDir, folder);
327
+
328
+ if (!existsSync(folderPath)) {
329
+ mkdirSync(folderPath, { recursive: true });
330
+ logInfoRaw(`Created folder: ${folder}`);
331
+ }
332
+ }
333
+
334
+ logInfoRaw("Additional folders created!");
335
+ }
336
+
337
+ async function setupPreCommitHooks(targetDir: string) {
338
+ const huskyDir = path.join(targetDir, folders.husky);
339
+ const preCommitHook = path.join(huskyDir, files.preCommit);
340
+ const commitMsgHook = path.join(huskyDir, files.commitMsg);
341
+
342
+ mkdirSync(huskyDir, { recursive: true });
343
+
344
+ await fsPromises.writeFile(preCommitHook, cliScripts.preCommit, "utf8");
345
+ await fsPromises.writeFile(commitMsgHook, cliScripts.commitMsg, "utf8");
346
+
347
+ runCommand(cliScripts.chmodPreCommit, targetDir);
348
+ runCommand(cliScripts.chmodCommitMsg, targetDir);
349
+ }
350
+
351
+ async function setupEslintConfig(targetDir: string) {
352
+ const eslintConfigMjsPath = path.join(targetDir, files.eslintConfigMjs);
353
+
354
+ if (existsSync(eslintConfigMjsPath)) {
355
+ await fsPromises.rm(eslintConfigMjsPath);
356
+ }
357
+
358
+ const eslintConfigJsPath = path.join(targetDir, files.eslintConfigJs);
359
+ await fsPromises.writeFile(eslintConfigJsPath, cliEslintContent, "utf8");
360
+ }
361
+
362
+ async function enableRepoModeExtras(targetDir: string) {
363
+ logInfoRaw("Enabling recommendation mode (-r)...");
364
+
365
+ runCommand(cliScripts.gitInit, targetDir);
366
+
367
+ runCommand(cliScripts.installDevHuskyTsx, targetDir);
368
+
369
+ runCommand(cliScripts.setLint, targetDir);
370
+
371
+ runCommand(cliScripts.installHusky, targetDir);
372
+
373
+ await setupPreCommitHooks(targetDir);
374
+ }
375
+
376
+ async function updateLayoutTitle(targetDir: string, folderName: string) {
377
+ const layoutPath = path.join(targetDir, `${projectPaths.app}${path.sep}${files.layout}`);
378
+
379
+ if (!existsSync(layoutPath)) {
380
+ throw createError(`${files.layout} not found at ${layoutPath}, cannot update title.`);
381
+ }
382
+
383
+ let content = await fsPromises.readFile(layoutPath, "utf8");
384
+ const titleRegex = /"SaaS Kit"/g;
385
+
386
+ if (!titleRegex.test(content)) {
387
+ throw createError(`SaaS Kit not found in ${files.layout}, cannot update title.`);
388
+ }
389
+
390
+ content = content.replace(titleRegex, `"${folderName}"`);
391
+ await fsPromises.writeFile(layoutPath, content, "utf8");
392
+ }
393
+
394
+ export async function create(args: string[]) {
395
+ try {
396
+ if (args.length < 1) {
397
+ logStringError("Error: Specify folder name. Example: npx create-saas-kit-app <folder-name>");
398
+ process.exit(1);
399
+ }
400
+
401
+ const folderName: string = args[0];
402
+ const enableRepoMode = args.includes("-r");
403
+ const targetDir: string = path.resolve(process.cwd(), folderName);
404
+
405
+ logInfoRaw(`\nCreating ${projectName} project "${folderName}"...\n`);
406
+
407
+ try {
408
+ checkTargetFolder(targetDir);
409
+ createNextProject(folderName, targetDir);
410
+ await copyProjectFiles(targetDir);
411
+ await overridePackageJson(targetDir);
412
+ createAdditionalFolders(targetDir);
413
+ await createEnvFile(targetDir);
414
+ await updateLayoutTitle(targetDir, folderName);
415
+ await setupEslintConfig(targetDir);
416
+
417
+ if (enableRepoMode) {
418
+ await enableRepoModeExtras(targetDir);
419
+ }
420
+
421
+ logInfoRaw(`\n${projectName} project "${folderName}" is ready!`);
422
+ logInfoRaw("Next steps:");
423
+ logInfoRaw(` cd ${folderName}`);
424
+ logInfoRaw(" npm run prod");
425
+ logInfoRaw(" npm start");
426
+ } catch (error) {
427
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
428
+ logStringError("Error:");
429
+ logStringError(errorMessage);
430
+
431
+ if (existsSync(targetDir)) {
432
+ logInfoRaw("Cleaning up...");
433
+ rmSync(targetDir, { recursive: true, force: true });
434
+ }
435
+
436
+ process.exit(1);
437
+ }
438
+ } catch (error) {
439
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
440
+ logStringError("Unhandled error:");
441
+ logStringError(errorMessage);
442
+ process.exit(1);
443
+ }
444
+ }