@bloomneo/appkit 1.2.9

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 (262) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +902 -0
  3. package/bin/appkit.js +71 -0
  4. package/bin/commands/generate.js +1050 -0
  5. package/bin/templates/backend/README.md.template +39 -0
  6. package/bin/templates/backend/api.http.template +0 -0
  7. package/bin/templates/backend/docs/APPKIT_CLI.md +507 -0
  8. package/bin/templates/backend/docs/APPKIT_COMMENTS_GUIDELINES.md +61 -0
  9. package/bin/templates/backend/docs/APPKIT_LLM_GUIDE.md +2539 -0
  10. package/bin/templates/backend/package.json.template +34 -0
  11. package/bin/templates/backend/src/api/features/welcome/welcome.http.template +29 -0
  12. package/bin/templates/backend/src/api/features/welcome/welcome.route.ts.template +36 -0
  13. package/bin/templates/backend/src/api/features/welcome/welcome.service.ts.template +88 -0
  14. package/bin/templates/backend/src/api/features/welcome/welcome.types.ts.template +18 -0
  15. package/bin/templates/backend/src/api/lib/api-router.ts.template +84 -0
  16. package/bin/templates/backend/src/api/server.ts.template +188 -0
  17. package/bin/templates/backend/tsconfig.api.json.template +24 -0
  18. package/bin/templates/backend/tsconfig.json.template +40 -0
  19. package/bin/templates/feature/feature.http.template +63 -0
  20. package/bin/templates/feature/feature.route.ts.template +36 -0
  21. package/bin/templates/feature/feature.service.ts.template +81 -0
  22. package/bin/templates/feature/feature.types.ts.template +23 -0
  23. package/bin/templates/feature-db/feature.http.template +63 -0
  24. package/bin/templates/feature-db/feature.model.ts.template +74 -0
  25. package/bin/templates/feature-db/feature.route.ts.template +58 -0
  26. package/bin/templates/feature-db/feature.service.ts.template +231 -0
  27. package/bin/templates/feature-db/feature.types.ts.template +25 -0
  28. package/bin/templates/feature-db/schema-addition.prisma.template +9 -0
  29. package/bin/templates/feature-db/seeding/README.md.template +57 -0
  30. package/bin/templates/feature-db/seeding/feature.seed.js.template +67 -0
  31. package/bin/templates/feature-user/schema-addition.prisma.template +19 -0
  32. package/bin/templates/feature-user/user.http.template +157 -0
  33. package/bin/templates/feature-user/user.model.ts.template +244 -0
  34. package/bin/templates/feature-user/user.route.ts.template +379 -0
  35. package/bin/templates/feature-user/user.seed.js.template +182 -0
  36. package/bin/templates/feature-user/user.service.ts.template +426 -0
  37. package/bin/templates/feature-user/user.types.ts.template +127 -0
  38. package/dist/auth/auth.d.ts +182 -0
  39. package/dist/auth/auth.d.ts.map +1 -0
  40. package/dist/auth/auth.js +477 -0
  41. package/dist/auth/auth.js.map +1 -0
  42. package/dist/auth/defaults.d.ts +104 -0
  43. package/dist/auth/defaults.d.ts.map +1 -0
  44. package/dist/auth/defaults.js +374 -0
  45. package/dist/auth/defaults.js.map +1 -0
  46. package/dist/auth/index.d.ts +70 -0
  47. package/dist/auth/index.d.ts.map +1 -0
  48. package/dist/auth/index.js +94 -0
  49. package/dist/auth/index.js.map +1 -0
  50. package/dist/cache/cache.d.ts +118 -0
  51. package/dist/cache/cache.d.ts.map +1 -0
  52. package/dist/cache/cache.js +249 -0
  53. package/dist/cache/cache.js.map +1 -0
  54. package/dist/cache/defaults.d.ts +63 -0
  55. package/dist/cache/defaults.d.ts.map +1 -0
  56. package/dist/cache/defaults.js +193 -0
  57. package/dist/cache/defaults.js.map +1 -0
  58. package/dist/cache/index.d.ts +101 -0
  59. package/dist/cache/index.d.ts.map +1 -0
  60. package/dist/cache/index.js +203 -0
  61. package/dist/cache/index.js.map +1 -0
  62. package/dist/cache/strategies/memory.d.ts +138 -0
  63. package/dist/cache/strategies/memory.d.ts.map +1 -0
  64. package/dist/cache/strategies/memory.js +348 -0
  65. package/dist/cache/strategies/memory.js.map +1 -0
  66. package/dist/cache/strategies/redis.d.ts +105 -0
  67. package/dist/cache/strategies/redis.d.ts.map +1 -0
  68. package/dist/cache/strategies/redis.js +318 -0
  69. package/dist/cache/strategies/redis.js.map +1 -0
  70. package/dist/config/config.d.ts +62 -0
  71. package/dist/config/config.d.ts.map +1 -0
  72. package/dist/config/config.js +107 -0
  73. package/dist/config/config.js.map +1 -0
  74. package/dist/config/defaults.d.ts +44 -0
  75. package/dist/config/defaults.d.ts.map +1 -0
  76. package/dist/config/defaults.js +217 -0
  77. package/dist/config/defaults.js.map +1 -0
  78. package/dist/config/index.d.ts +105 -0
  79. package/dist/config/index.d.ts.map +1 -0
  80. package/dist/config/index.js +163 -0
  81. package/dist/config/index.js.map +1 -0
  82. package/dist/database/adapters/mongoose.d.ts +106 -0
  83. package/dist/database/adapters/mongoose.d.ts.map +1 -0
  84. package/dist/database/adapters/mongoose.js +480 -0
  85. package/dist/database/adapters/mongoose.js.map +1 -0
  86. package/dist/database/adapters/prisma.d.ts +106 -0
  87. package/dist/database/adapters/prisma.d.ts.map +1 -0
  88. package/dist/database/adapters/prisma.js +494 -0
  89. package/dist/database/adapters/prisma.js.map +1 -0
  90. package/dist/database/defaults.d.ts +87 -0
  91. package/dist/database/defaults.d.ts.map +1 -0
  92. package/dist/database/defaults.js +271 -0
  93. package/dist/database/defaults.js.map +1 -0
  94. package/dist/database/index.d.ts +137 -0
  95. package/dist/database/index.d.ts.map +1 -0
  96. package/dist/database/index.js +490 -0
  97. package/dist/database/index.js.map +1 -0
  98. package/dist/email/defaults.d.ts +100 -0
  99. package/dist/email/defaults.d.ts.map +1 -0
  100. package/dist/email/defaults.js +400 -0
  101. package/dist/email/defaults.js.map +1 -0
  102. package/dist/email/email.d.ts +139 -0
  103. package/dist/email/email.d.ts.map +1 -0
  104. package/dist/email/email.js +316 -0
  105. package/dist/email/email.js.map +1 -0
  106. package/dist/email/index.d.ts +176 -0
  107. package/dist/email/index.d.ts.map +1 -0
  108. package/dist/email/index.js +251 -0
  109. package/dist/email/index.js.map +1 -0
  110. package/dist/email/strategies/console.d.ts +90 -0
  111. package/dist/email/strategies/console.d.ts.map +1 -0
  112. package/dist/email/strategies/console.js +268 -0
  113. package/dist/email/strategies/console.js.map +1 -0
  114. package/dist/email/strategies/resend.d.ts +84 -0
  115. package/dist/email/strategies/resend.d.ts.map +1 -0
  116. package/dist/email/strategies/resend.js +266 -0
  117. package/dist/email/strategies/resend.js.map +1 -0
  118. package/dist/email/strategies/smtp.d.ts +77 -0
  119. package/dist/email/strategies/smtp.d.ts.map +1 -0
  120. package/dist/email/strategies/smtp.js +286 -0
  121. package/dist/email/strategies/smtp.js.map +1 -0
  122. package/dist/error/defaults.d.ts +40 -0
  123. package/dist/error/defaults.d.ts.map +1 -0
  124. package/dist/error/defaults.js +75 -0
  125. package/dist/error/defaults.js.map +1 -0
  126. package/dist/error/error.d.ts +140 -0
  127. package/dist/error/error.d.ts.map +1 -0
  128. package/dist/error/error.js +200 -0
  129. package/dist/error/error.js.map +1 -0
  130. package/dist/error/index.d.ts +145 -0
  131. package/dist/error/index.d.ts.map +1 -0
  132. package/dist/error/index.js +145 -0
  133. package/dist/error/index.js.map +1 -0
  134. package/dist/event/defaults.d.ts +111 -0
  135. package/dist/event/defaults.d.ts.map +1 -0
  136. package/dist/event/defaults.js +378 -0
  137. package/dist/event/defaults.js.map +1 -0
  138. package/dist/event/event.d.ts +171 -0
  139. package/dist/event/event.d.ts.map +1 -0
  140. package/dist/event/event.js +391 -0
  141. package/dist/event/event.js.map +1 -0
  142. package/dist/event/index.d.ts +173 -0
  143. package/dist/event/index.d.ts.map +1 -0
  144. package/dist/event/index.js +302 -0
  145. package/dist/event/index.js.map +1 -0
  146. package/dist/event/strategies/memory.d.ts +122 -0
  147. package/dist/event/strategies/memory.d.ts.map +1 -0
  148. package/dist/event/strategies/memory.js +331 -0
  149. package/dist/event/strategies/memory.js.map +1 -0
  150. package/dist/event/strategies/redis.d.ts +115 -0
  151. package/dist/event/strategies/redis.d.ts.map +1 -0
  152. package/dist/event/strategies/redis.js +434 -0
  153. package/dist/event/strategies/redis.js.map +1 -0
  154. package/dist/index.d.ts +58 -0
  155. package/dist/index.d.ts.map +1 -0
  156. package/dist/index.js +72 -0
  157. package/dist/index.js.map +1 -0
  158. package/dist/logger/defaults.d.ts +67 -0
  159. package/dist/logger/defaults.d.ts.map +1 -0
  160. package/dist/logger/defaults.js +213 -0
  161. package/dist/logger/defaults.js.map +1 -0
  162. package/dist/logger/index.d.ts +84 -0
  163. package/dist/logger/index.d.ts.map +1 -0
  164. package/dist/logger/index.js +101 -0
  165. package/dist/logger/index.js.map +1 -0
  166. package/dist/logger/logger.d.ts +165 -0
  167. package/dist/logger/logger.d.ts.map +1 -0
  168. package/dist/logger/logger.js +843 -0
  169. package/dist/logger/logger.js.map +1 -0
  170. package/dist/logger/transports/console.d.ts +102 -0
  171. package/dist/logger/transports/console.d.ts.map +1 -0
  172. package/dist/logger/transports/console.js +276 -0
  173. package/dist/logger/transports/console.js.map +1 -0
  174. package/dist/logger/transports/database.d.ts +153 -0
  175. package/dist/logger/transports/database.d.ts.map +1 -0
  176. package/dist/logger/transports/database.js +539 -0
  177. package/dist/logger/transports/database.js.map +1 -0
  178. package/dist/logger/transports/file.d.ts +146 -0
  179. package/dist/logger/transports/file.d.ts.map +1 -0
  180. package/dist/logger/transports/file.js +464 -0
  181. package/dist/logger/transports/file.js.map +1 -0
  182. package/dist/logger/transports/http.d.ts +128 -0
  183. package/dist/logger/transports/http.d.ts.map +1 -0
  184. package/dist/logger/transports/http.js +401 -0
  185. package/dist/logger/transports/http.js.map +1 -0
  186. package/dist/logger/transports/webhook.d.ts +152 -0
  187. package/dist/logger/transports/webhook.d.ts.map +1 -0
  188. package/dist/logger/transports/webhook.js +485 -0
  189. package/dist/logger/transports/webhook.js.map +1 -0
  190. package/dist/queue/defaults.d.ts +66 -0
  191. package/dist/queue/defaults.d.ts.map +1 -0
  192. package/dist/queue/defaults.js +205 -0
  193. package/dist/queue/defaults.js.map +1 -0
  194. package/dist/queue/index.d.ts +124 -0
  195. package/dist/queue/index.d.ts.map +1 -0
  196. package/dist/queue/index.js +116 -0
  197. package/dist/queue/index.js.map +1 -0
  198. package/dist/queue/queue.d.ts +156 -0
  199. package/dist/queue/queue.d.ts.map +1 -0
  200. package/dist/queue/queue.js +387 -0
  201. package/dist/queue/queue.js.map +1 -0
  202. package/dist/queue/transports/database.d.ts +165 -0
  203. package/dist/queue/transports/database.d.ts.map +1 -0
  204. package/dist/queue/transports/database.js +595 -0
  205. package/dist/queue/transports/database.js.map +1 -0
  206. package/dist/queue/transports/memory.d.ts +143 -0
  207. package/dist/queue/transports/memory.d.ts.map +1 -0
  208. package/dist/queue/transports/memory.js +415 -0
  209. package/dist/queue/transports/memory.js.map +1 -0
  210. package/dist/queue/transports/redis.d.ts +203 -0
  211. package/dist/queue/transports/redis.d.ts.map +1 -0
  212. package/dist/queue/transports/redis.js +744 -0
  213. package/dist/queue/transports/redis.js.map +1 -0
  214. package/dist/security/defaults.d.ts +64 -0
  215. package/dist/security/defaults.d.ts.map +1 -0
  216. package/dist/security/defaults.js +159 -0
  217. package/dist/security/defaults.js.map +1 -0
  218. package/dist/security/index.d.ts +110 -0
  219. package/dist/security/index.d.ts.map +1 -0
  220. package/dist/security/index.js +160 -0
  221. package/dist/security/index.js.map +1 -0
  222. package/dist/security/security.d.ts +138 -0
  223. package/dist/security/security.d.ts.map +1 -0
  224. package/dist/security/security.js +419 -0
  225. package/dist/security/security.js.map +1 -0
  226. package/dist/storage/defaults.d.ts +79 -0
  227. package/dist/storage/defaults.d.ts.map +1 -0
  228. package/dist/storage/defaults.js +358 -0
  229. package/dist/storage/defaults.js.map +1 -0
  230. package/dist/storage/index.d.ts +153 -0
  231. package/dist/storage/index.d.ts.map +1 -0
  232. package/dist/storage/index.js +242 -0
  233. package/dist/storage/index.js.map +1 -0
  234. package/dist/storage/storage.d.ts +151 -0
  235. package/dist/storage/storage.d.ts.map +1 -0
  236. package/dist/storage/storage.js +439 -0
  237. package/dist/storage/storage.js.map +1 -0
  238. package/dist/storage/strategies/local.d.ts +117 -0
  239. package/dist/storage/strategies/local.d.ts.map +1 -0
  240. package/dist/storage/strategies/local.js +368 -0
  241. package/dist/storage/strategies/local.js.map +1 -0
  242. package/dist/storage/strategies/r2.d.ts +130 -0
  243. package/dist/storage/strategies/r2.d.ts.map +1 -0
  244. package/dist/storage/strategies/r2.js +470 -0
  245. package/dist/storage/strategies/r2.js.map +1 -0
  246. package/dist/storage/strategies/s3.d.ts +121 -0
  247. package/dist/storage/strategies/s3.d.ts.map +1 -0
  248. package/dist/storage/strategies/s3.js +461 -0
  249. package/dist/storage/strategies/s3.js.map +1 -0
  250. package/dist/util/defaults.d.ts +77 -0
  251. package/dist/util/defaults.d.ts.map +1 -0
  252. package/dist/util/defaults.js +193 -0
  253. package/dist/util/defaults.js.map +1 -0
  254. package/dist/util/index.d.ts +97 -0
  255. package/dist/util/index.d.ts.map +1 -0
  256. package/dist/util/index.js +165 -0
  257. package/dist/util/index.js.map +1 -0
  258. package/dist/util/util.d.ts +145 -0
  259. package/dist/util/util.d.ts.map +1 -0
  260. package/dist/util/util.js +481 -0
  261. package/dist/util/util.js.map +1 -0
  262. package/package.json +234 -0
@@ -0,0 +1,1050 @@
1
+ /**
2
+ * @fileoverview AppKit Generate Command - Smart project and feature scaffolding
3
+ * @description Generates apps, features, and components using FBCA pattern with AppKit modules
4
+ * @file appkit/bin/commands/generate.js
5
+ */
6
+
7
+ import fs from 'fs/promises';
8
+ import path from 'path';
9
+ import { spawn } from 'child_process';
10
+ import { fileURLToPath } from 'url';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+
15
+ /**
16
+ * Main generate command router
17
+ */
18
+ export async function generate(type, name, options = {}) {
19
+ // Smart folder name detection if no name provided
20
+ if (!name && type === 'app') {
21
+ name = path.basename(process.cwd());
22
+ console.log(`🔍 Detected project name: "${name}"`);
23
+ }
24
+
25
+ switch (type) {
26
+ case 'app':
27
+ await generateApp(name, options);
28
+ break;
29
+ case 'feature':
30
+ await generateFeature(name, options);
31
+ break;
32
+ default:
33
+ console.error(`❌ Unknown type "${type}". Use: app, feature`);
34
+ console.log(`\n💡 Available commands:`);
35
+ console.log(
36
+ ` npx appkit generate app [name] - Create full AppKit project`
37
+ );
38
+ console.log(
39
+ ` npx appkit generate feature <name> - Add feature to existing project`
40
+ );
41
+ process.exit(1);
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Generate a complete AppKit application with smart context detection
47
+ */
48
+ async function generateApp(name, options) {
49
+ try {
50
+ const currentDir = process.cwd();
51
+ const isCurrentDir = !name || name === path.basename(currentDir);
52
+ const projectPath = isCurrentDir
53
+ ? currentDir
54
+ : path.resolve(currentDir, name);
55
+ const projectName = name || path.basename(currentDir);
56
+
57
+ console.log(
58
+ `📁 Creating AppKit structure${isCurrentDir ? ' in current directory' : ` in "${name}"`}...`
59
+ );
60
+
61
+ const templatesPath = path.join(__dirname, '..', 'templates', 'backend');
62
+ const createdFiles = [];
63
+ const skippedFiles = [];
64
+
65
+ // Check if we're adding to existing project
66
+ const existingApiPath = path.join(projectPath, 'src', 'api');
67
+ const hasExistingApi = await fileExists(existingApiPath);
68
+
69
+ if (hasExistingApi) {
70
+ console.log(
71
+ '🔍 Detected existing src/api structure, adding missing files only...\n'
72
+ );
73
+ }
74
+
75
+ // Create project directory if needed
76
+ if (!isCurrentDir) {
77
+ try {
78
+ await fs.access(projectPath);
79
+ console.log(
80
+ `⚠️ Directory "${name}" already exists, adding files safely...`
81
+ );
82
+ } catch {
83
+ await fs.mkdir(projectPath, { recursive: true });
84
+ createdFiles.push(`📁 ${name}/`);
85
+ }
86
+ }
87
+
88
+ // Generate shared random keys for the project
89
+ const randomFrontendKey =
90
+ 'voila_' +
91
+ Math.random().toString(36).substring(2, 15) +
92
+ Math.random().toString(36).substring(2, 15);
93
+ const randomAuthSecret =
94
+ 'auth_' +
95
+ Math.random().toString(36).substring(2, 15) +
96
+ Math.random().toString(36).substring(2, 15) +
97
+ Math.random().toString(36).substring(2, 15);
98
+ const randomDefaultPassword =
99
+ Math.random().toString(36).substring(2, 8) +
100
+ Math.random().toString(36).substring(2, 6);
101
+
102
+ // Copy backend structure with smart file handling
103
+ await copyDirectorySafe(
104
+ templatesPath,
105
+ projectPath,
106
+ projectName,
107
+ createdFiles,
108
+ skippedFiles,
109
+ ['api.http.template'],
110
+ randomFrontendKey,
111
+ randomAuthSecret,
112
+ randomDefaultPassword
113
+ );
114
+
115
+ // Handle package.json smartly
116
+ await handlePackageJson(
117
+ projectPath,
118
+ projectName,
119
+ createdFiles,
120
+ skippedFiles
121
+ );
122
+
123
+ // Report results
124
+ console.log(`\n📊 Summary:`);
125
+ if (createdFiles.length > 0) {
126
+ console.log(`✅ Created ${createdFiles.length} files:`);
127
+ createdFiles.forEach((file) => console.log(` ${file}`));
128
+ }
129
+ if (skippedFiles.length > 0) {
130
+ console.log(`⚠️ Skipped ${skippedFiles.length} existing files:`);
131
+ skippedFiles.forEach((file) => console.log(` ${file}`));
132
+ }
133
+
134
+ // Install dependencies if package.json was created/updated
135
+ if (
136
+ createdFiles.some((f) => f.includes('package.json')) ||
137
+ createdFiles.length > 2
138
+ ) {
139
+ console.log(`\n📦 Installing dependencies...`);
140
+ await installDependencies(projectPath);
141
+ }
142
+
143
+ // Success message
144
+ console.log(`\n🚀 AppKit project ready!`);
145
+ console.log(`\n💡 Next steps:`);
146
+ if (!isCurrentDir) {
147
+ console.log(` cd ${name}`);
148
+ }
149
+ console.log(` npm run dev:api`);
150
+ console.log(
151
+ `\n🌐 Your API will be available at: http://localhost:3000/api`
152
+ );
153
+ } catch (error) {
154
+ console.error('❌ Failed to generate app:', error.message);
155
+ process.exit(1);
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Generate a new feature in existing project
161
+ */
162
+ async function generateFeature(name, options) {
163
+ const withDb = options && options.db;
164
+ const isUserFeature = name === 'user';
165
+
166
+ console.log(
167
+ `🔧 Generating ${isUserFeature ? 'user authentication feature' : `feature: "${name}"`}${withDb ? ' with database support' : ''}...\n`
168
+ );
169
+
170
+ try {
171
+ // Validate feature name
172
+ if (!name || !/^[a-zA-Z0-9-_]+$/.test(name)) {
173
+ console.error(
174
+ '❌ Invalid feature name. Use only letters, numbers, hyphens, and underscores.'
175
+ );
176
+ process.exit(1);
177
+ }
178
+
179
+ // Check if we're in a project directory
180
+ const currentDir = process.cwd();
181
+ const packageJsonPath = path.join(currentDir, 'package.json');
182
+
183
+ try {
184
+ const packageJson = JSON.parse(
185
+ await fs.readFile(packageJsonPath, 'utf8')
186
+ );
187
+ if (
188
+ !packageJson.dependencies ||
189
+ !packageJson.dependencies['@bloomneo/appkit']
190
+ ) {
191
+ console.error(
192
+ '❌ Not in an AppKit project directory. Run this from project root.'
193
+ );
194
+ console.log('💡 First run: npx appkit generate app');
195
+ process.exit(1);
196
+ }
197
+ } catch {
198
+ console.error('❌ No package.json found. Run this from project root.');
199
+ process.exit(1);
200
+ }
201
+
202
+ // Validate FBCA structure
203
+ const featuresPath = await validateFBCAStructure(currentDir);
204
+
205
+ // Check if feature already exists
206
+ const featurePath = path.join(featuresPath, name);
207
+ try {
208
+ await fs.access(featurePath);
209
+ console.error(`❌ Feature "${name}" already exists.`);
210
+ process.exit(1);
211
+ } catch {
212
+ // Feature doesn't exist, good to proceed
213
+ }
214
+
215
+ // Generate feature based on type
216
+ if (isUserFeature) {
217
+ await generateUserFeature(featuresPath, name, currentDir);
218
+ } else {
219
+ // Generate regular feature scaffolding
220
+ await generateFeatureScaffolding(featuresPath, name, options);
221
+
222
+ // Handle database integration if --db flag is used
223
+ if (withDb) {
224
+ await handleDatabaseIntegration(currentDir, name);
225
+ }
226
+ }
227
+
228
+ console.log(
229
+ `✅ Generated ${isUserFeature ? 'user authentication feature' : `feature "${name}"`} successfully!`
230
+ );
231
+ console.log(`\n📁 Files created:`);
232
+
233
+ if (isUserFeature) {
234
+ console.log(` src/api/features/user/user.route.ts`);
235
+ console.log(` src/api/features/user/user.service.ts`);
236
+ console.log(` src/api/features/user/user.types.ts`);
237
+ console.log(` src/api/features/user/user.model.ts`);
238
+ console.log(` src/api/features/user/user.http`);
239
+ console.log(` prisma/seeding/user.seed.js`);
240
+ console.log(` prisma/schema.prisma (User model added)`);
241
+ } else {
242
+ console.log(` src/api/features/${name}/${name}.route.ts`);
243
+ console.log(` src/api/features/${name}/${name}.service.ts`);
244
+ console.log(` src/api/features/${name}/${name}.types.ts`);
245
+ if (withDb) {
246
+ console.log(` src/api/features/${name}/${name}.model.ts`);
247
+ console.log(` src/api/features/${name}/${name}.http`);
248
+ console.log(` prisma/seeding/${name}.seed.js`);
249
+ }
250
+ }
251
+
252
+ console.log(`\n🚀 Feature available at: /api/${name}`);
253
+ console.log(`\n💡 Next steps:`);
254
+
255
+ if (isUserFeature) {
256
+ console.log(
257
+ ` 1. Install dependencies: npm install prisma @prisma/client bcrypt`
258
+ );
259
+ console.log(
260
+ ` 2. Install dev dependencies: npm install -D @types/bcrypt`
261
+ );
262
+ console.log(` 3. Create database: npx prisma db push`);
263
+ console.log(` 4. Generate Prisma client: npx prisma generate`);
264
+ console.log(` 5. Seed user accounts: node prisma/seeding/user.seed.js`);
265
+ console.log(` 6. Test authentication: Use user.http file in VS Code`);
266
+ console.log(` 7. Start server: npm run dev:api`);
267
+ console.log(
268
+ `\n🔐 Complete authentication system with 9 role accounts ready!`
269
+ );
270
+ console.log(`🧪 Default password for all test accounts: Password123!`);
271
+ } else if (withDb) {
272
+ console.log(
273
+ ` 1. Install Prisma if needed: npm install prisma @prisma/client`
274
+ );
275
+ console.log(` 2. Create database: npx prisma db push`);
276
+ console.log(` 3. Generate client: npx prisma generate`);
277
+ console.log(` 4. Seed data: node prisma/seeding/${name}.seed.js`);
278
+ console.log(` 5. Test API: Use ${name}.http file in VS Code`);
279
+ console.log(` 6. Start server: npm run dev:api`);
280
+ console.log(`\n🌱 Manual seeding gives you full control over your data!`);
281
+ } else {
282
+ console.log(` 1. Update ${name}.types.ts with your data types`);
283
+ console.log(` 2. Implement business logic in ${name}.service.ts`);
284
+ console.log(` 3. Test your API: curl http://localhost:3000/api/${name}`);
285
+ }
286
+ } catch (error) {
287
+ console.error('❌ Failed to generate feature:', error.message);
288
+ process.exit(1);
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Copy directory recursively with safe non-destructive behavior
294
+ */
295
+ async function copyDirectorySafe(
296
+ src,
297
+ dest,
298
+ projectName,
299
+ createdFiles,
300
+ skippedFiles,
301
+ excludeFiles = [],
302
+ sharedFrontendKey = null,
303
+ sharedAuthSecret = null,
304
+ sharedDefaultPassword = null
305
+ ) {
306
+ await fs.mkdir(dest, { recursive: true });
307
+
308
+ const entries = await fs.readdir(src, { withFileTypes: true });
309
+
310
+ for (const entry of entries) {
311
+ // Skip excluded files
312
+ if (excludeFiles.includes(entry.name)) {
313
+ continue;
314
+ }
315
+
316
+ const srcPath = path.join(src, entry.name);
317
+ const destPath = path.join(dest, entry.name);
318
+
319
+ if (entry.isDirectory()) {
320
+ await copyDirectorySafe(
321
+ srcPath,
322
+ destPath,
323
+ projectName,
324
+ createdFiles,
325
+ skippedFiles,
326
+ excludeFiles,
327
+ sharedFrontendKey,
328
+ sharedAuthSecret
329
+ );
330
+ } else {
331
+ // Remove .template extension for final path
332
+ const finalDestPath = destPath.endsWith('.template')
333
+ ? destPath.slice(0, -9)
334
+ : destPath;
335
+
336
+ // Check if file already exists
337
+ const exists = await fileExists(finalDestPath);
338
+ if (exists) {
339
+ skippedFiles.push(path.relative(dest, finalDestPath));
340
+ continue;
341
+ }
342
+
343
+ // Read and process template
344
+ let content = await fs.readFile(srcPath, 'utf8');
345
+
346
+ // Use shared keys or generate them if not provided
347
+ const frontendKey =
348
+ sharedFrontendKey ||
349
+ 'voila_' +
350
+ Math.random().toString(36).substring(2, 15) +
351
+ Math.random().toString(36).substring(2, 15);
352
+ const authSecret =
353
+ sharedAuthSecret ||
354
+ 'auth_' +
355
+ Math.random().toString(36).substring(2, 15) +
356
+ Math.random().toString(36).substring(2, 15) +
357
+ Math.random().toString(36).substring(2, 15);
358
+ const defaultPassword =
359
+ sharedDefaultPassword ||
360
+ Math.random().toString(36).substring(2, 8) +
361
+ Math.random().toString(36).substring(2, 6);
362
+
363
+ content = content
364
+ .replace(/\{\{projectName\}\}/g, projectName)
365
+ .replace(/\{\{randomFrontendKey\}\}/g, frontendKey)
366
+ .replace(/\{\{frontendKey\}\}/g, frontendKey)
367
+ .replace(/\{\{randomAuthSecret\}\}/g, authSecret)
368
+ .replace(/\{\{randomDefaultPassword\}\}/g, defaultPassword);
369
+
370
+ // Write file
371
+ await fs.writeFile(finalDestPath, content);
372
+ createdFiles.push(path.relative(dest, finalDestPath));
373
+ }
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Handle package.json creation/updating smartly
379
+ */
380
+ async function handlePackageJson(
381
+ projectPath,
382
+ projectName,
383
+ createdFiles,
384
+ skippedFiles
385
+ ) {
386
+ const packageJsonPath = path.join(projectPath, 'package.json');
387
+ const exists = await fileExists(packageJsonPath);
388
+
389
+ if (exists) {
390
+ // Update existing package.json
391
+ try {
392
+ const packageContent = await fs.readFile(packageJsonPath, 'utf8');
393
+ const packageJson = JSON.parse(packageContent);
394
+ let updated = false;
395
+
396
+ // Add backend dependencies if missing
397
+ packageJson.dependencies = packageJson.dependencies || {};
398
+ if (!packageJson.dependencies['@bloomneo/appkit']) {
399
+ packageJson.dependencies['@bloomneo/appkit'] = '^1.0.0';
400
+ updated = true;
401
+ }
402
+ if (!packageJson.dependencies['express']) {
403
+ packageJson.dependencies['express'] = '^4.18.2';
404
+ updated = true;
405
+ }
406
+ if (!packageJson.dependencies['cors']) {
407
+ packageJson.dependencies['cors'] = '^2.8.5';
408
+ updated = true;
409
+ }
410
+
411
+ packageJson.devDependencies = packageJson.devDependencies || {};
412
+ if (!packageJson.devDependencies['nodemon']) {
413
+ packageJson.devDependencies['nodemon'] = '^3.0.1';
414
+ updated = true;
415
+ }
416
+ if (!packageJson.devDependencies['tsx']) {
417
+ packageJson.devDependencies['tsx'] = '^4.20.5';
418
+ updated = true;
419
+ }
420
+
421
+ // Add scripts if they don't exist
422
+ packageJson.scripts = packageJson.scripts || {};
423
+ if (!packageJson.scripts['dev:api']) {
424
+ packageJson.scripts['dev:api'] =
425
+ 'API_ONLY=true nodemon --exec tsx src/api/server.ts';
426
+ updated = true;
427
+ }
428
+ if (!packageJson.scripts['build:api']) {
429
+ packageJson.scripts['build:api'] = 'tsc --project tsconfig.api.json';
430
+ updated = true;
431
+ }
432
+ if (!packageJson.scripts['start:api']) {
433
+ packageJson.scripts['start:api'] = 'node dist/api/server.js';
434
+ updated = true;
435
+ }
436
+
437
+ if (updated) {
438
+ await fs.writeFile(
439
+ packageJsonPath,
440
+ JSON.stringify(packageJson, null, 2)
441
+ );
442
+ createdFiles.push('package.json (updated)');
443
+ } else {
444
+ skippedFiles.push('package.json (no changes needed)');
445
+ }
446
+ } catch (error) {
447
+ skippedFiles.push('package.json (update failed)');
448
+ }
449
+ }
450
+ // If package.json doesn't exist, it will be created by copyDirectorySafe
451
+ }
452
+
453
+ /**
454
+ * Generate complete feature scaffolding using TypeScript templates
455
+ */
456
+ async function generateFeatureScaffolding(featuresPath, name, options) {
457
+ const featurePath = path.join(featuresPath, name);
458
+ await fs.mkdir(featurePath, { recursive: true });
459
+
460
+ // Choose template path based on --db flag
461
+ const withDb = options && options.db;
462
+ const templateDir = withDb ? 'feature-db' : 'feature';
463
+ const templatesPath = path.join(__dirname, `../templates/${templateDir}`);
464
+
465
+ try {
466
+ // Generate core feature files
467
+ await generateFromTemplate(
468
+ templatesPath,
469
+ 'feature.route.ts.template',
470
+ featurePath,
471
+ `${name}.route.ts`,
472
+ name
473
+ );
474
+ await generateFromTemplate(
475
+ templatesPath,
476
+ 'feature.service.ts.template',
477
+ featurePath,
478
+ `${name}.service.ts`,
479
+ name
480
+ );
481
+ await generateFromTemplate(
482
+ templatesPath,
483
+ 'feature.types.ts.template',
484
+ featurePath,
485
+ `${name}.types.ts`,
486
+ name
487
+ );
488
+
489
+ // Generate .http file for all features
490
+ await generateFromTemplate(
491
+ templatesPath,
492
+ 'feature.http.template',
493
+ featurePath,
494
+ `${name}.http`,
495
+ name
496
+ );
497
+
498
+ console.log(` ✅ Generated ${name}.route.ts`);
499
+ console.log(` ✅ Generated ${name}.service.ts`);
500
+ console.log(` ✅ Generated ${name}.types.ts`);
501
+ console.log(` ✅ Generated ${name}.http`);
502
+
503
+ // Generate additional files for database features
504
+ if (withDb) {
505
+ await generateFromTemplate(
506
+ templatesPath,
507
+ 'feature.model.ts.template',
508
+ featurePath,
509
+ `${name}.model.ts`,
510
+ name
511
+ );
512
+ console.log(` ✅ Generated ${name}.model.ts`);
513
+
514
+ // Generate seeding files
515
+ await generateSeedingFiles(templatesPath, name);
516
+ }
517
+
518
+ console.log(
519
+ `✅ Created feature directory: ${name}/ ${withDb ? '(with database support)' : ''}`
520
+ );
521
+ } catch (error) {
522
+ console.error(
523
+ `❌ Failed to generate feature from templates: ${error.message}`
524
+ );
525
+ throw error;
526
+ }
527
+ }
528
+
529
+ /**
530
+ * Generate file from template with variable replacement
531
+ */
532
+ async function generateFromTemplate(
533
+ templatesPath,
534
+ templateFile,
535
+ outputPath,
536
+ outputFile,
537
+ featureName
538
+ ) {
539
+ try {
540
+ // Read template file
541
+ const templatePath = path.join(templatesPath, templateFile);
542
+ const templateContent = await fs.readFile(templatePath, 'utf8');
543
+
544
+ // Get project name from package.json
545
+ const currentDir = process.cwd();
546
+ const packageJsonPath = path.join(currentDir, 'package.json');
547
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
548
+ const projectName = packageJson.name || path.basename(currentDir);
549
+
550
+ // Get keys from .env file
551
+ let frontendKey = 'frontend_dev_2024_test_key_12345'; // default
552
+ let authSecret = 'auth_default_secret_12345678901234567890'; // default
553
+ let defaultPassword = 'default123'; // default
554
+ try {
555
+ const envPath = path.join(currentDir, '.env');
556
+ const envContent = await fs.readFile(envPath, 'utf8');
557
+ const keyMatch = envContent.match(
558
+ /VOILA_FRONTEND_KEY\s*=\s*["']?([^"'\n\r]+)["']?/
559
+ );
560
+ if (keyMatch) {
561
+ frontendKey = keyMatch[1];
562
+ }
563
+ const authMatch = envContent.match(
564
+ /VOILA_AUTH_SECRET\s*=\s*["']?([^"'\n\r]+)["']?/
565
+ );
566
+ if (authMatch) {
567
+ authSecret = authMatch[1];
568
+ }
569
+ const passwordMatch = envContent.match(
570
+ /DEFAULT_USER_PASSWORD\s*=\s*["']?([^"'\n\r]+)["']?/
571
+ );
572
+ if (passwordMatch) {
573
+ defaultPassword = passwordMatch[1];
574
+ }
575
+ } catch (error) {
576
+ // Use defaults if .env doesn't exist or can't be read
577
+ }
578
+
579
+ // Replace template variables
580
+ const processedContent = templateContent
581
+ .replace(/\{\{featureName\}\}/g, featureName)
582
+ .replace(
583
+ /\{\{FeatureName\}\}/g,
584
+ featureName.charAt(0).toUpperCase() + featureName.slice(1)
585
+ )
586
+ .replace(/\{\{tableName\}\}/g, featureName)
587
+ .replace(/\{\{projectName\}\}/g, projectName)
588
+ .replace(/\{\{frontendKey\}\}/g, frontendKey)
589
+ .replace(/\{\{randomAuthSecret\}\}/g, authSecret)
590
+ .replace(/\{\{randomDefaultPassword\}\}/g, defaultPassword);
591
+
592
+ // Write output file
593
+ const outputFilePath = path.join(outputPath, outputFile);
594
+ await fs.writeFile(outputFilePath, processedContent, 'utf8');
595
+ } catch (error) {
596
+ console.error(
597
+ `❌ Failed to generate ${outputFile} from template ${templateFile}:`,
598
+ error.message
599
+ );
600
+ throw error;
601
+ }
602
+ }
603
+
604
+ /**
605
+ * Handle database integration for --db flag
606
+ */
607
+ async function handleDatabaseIntegration(projectDir, featureName) {
608
+ try {
609
+ console.log(`🗄️ Setting up database integration for ${featureName}...`);
610
+
611
+ // Check if Prisma is installed
612
+ const packageJsonPath = path.join(projectDir, 'package.json');
613
+ const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
614
+
615
+ if (
616
+ !packageJson.dependencies?.prisma &&
617
+ !packageJson.devDependencies?.prisma
618
+ ) {
619
+ console.log(`📦 Installing Prisma...`);
620
+ console.log(`⚠️ Please run: npm install prisma @prisma/client`);
621
+ }
622
+
623
+ // Check if prisma/schema.prisma exists
624
+ const schemaPath = path.join(projectDir, 'prisma/schema.prisma');
625
+ const schemaExists = await fileExists(schemaPath);
626
+
627
+ if (!schemaExists) {
628
+ // Create prisma directory and basic schema with first model
629
+ await fs.mkdir(path.join(projectDir, 'prisma'), { recursive: true });
630
+
631
+ const basicSchema = `// This is your Prisma schema file,
632
+ // learn more about it in the docs: https://pris.ly/d/prisma-schema
633
+
634
+ generator client {
635
+ provider = "prisma-client-js"
636
+ }
637
+
638
+ datasource db {
639
+ provider = "sqlite"
640
+ url = env("DATABASE_URL")
641
+ }
642
+
643
+ model ${featureName.charAt(0).toUpperCase() + featureName.slice(1)} {
644
+ id Int @id @default(autoincrement())
645
+ name String
646
+ createdAt DateTime @default(now())
647
+ updatedAt DateTime @updatedAt
648
+ }
649
+ `;
650
+
651
+ await fs.writeFile(schemaPath, basicSchema);
652
+ console.log(`✅ Created prisma/schema.prisma with ${featureName} model`);
653
+ } else {
654
+ // Schema exists, check if model already exists
655
+ const schemaContent = await fs.readFile(schemaPath, 'utf8');
656
+ const modelName =
657
+ featureName.charAt(0).toUpperCase() + featureName.slice(1);
658
+ const modelPattern = new RegExp(`model\\s+${modelName}\\s*\\{`, 'i');
659
+
660
+ if (modelPattern.test(schemaContent)) {
661
+ console.log(
662
+ `⚠️ Model "${modelName}" already exists in schema. Skipping...`
663
+ );
664
+ } else {
665
+ // Append new model to existing schema
666
+ const newModel = `
667
+ model ${modelName} {
668
+ id Int @id @default(autoincrement())
669
+ name String
670
+ createdAt DateTime @default(now())
671
+ updatedAt DateTime @updatedAt
672
+ }
673
+ `;
674
+
675
+ await fs.appendFile(schemaPath, newModel);
676
+ console.log(`✅ Added ${modelName} model to existing schema`);
677
+ }
678
+ }
679
+
680
+ console.log(
681
+ `📊 Database integration ready! Run 'npx prisma generate' to update the client.`
682
+ );
683
+ } catch (error) {
684
+ console.error(`❌ Failed to setup database integration: ${error.message}`);
685
+ throw error;
686
+ }
687
+ }
688
+
689
+ /**
690
+ * Generate seeding files for database features
691
+ */
692
+ async function generateSeedingFiles(templatesPath, featureName) {
693
+ try {
694
+ const currentDir = process.cwd();
695
+ const seedingDir = path.join(currentDir, 'prisma', 'seeding');
696
+
697
+ // Create seeding directory if it doesn't exist
698
+ await fs.mkdir(seedingDir, { recursive: true });
699
+
700
+ // Generate feature seed file
701
+ const seedingTemplatesPath = path.join(templatesPath, 'seeding');
702
+ await generateFromTemplate(
703
+ seedingTemplatesPath,
704
+ 'feature.seed.js.template',
705
+ seedingDir,
706
+ `${featureName}.seed.js`,
707
+ featureName
708
+ );
709
+
710
+ // Generate README if it doesn't exist
711
+ const readmePath = path.join(seedingDir, 'README.md');
712
+ const readmeExists = await fileExists(readmePath);
713
+ if (!readmeExists) {
714
+ await generateFromTemplate(
715
+ seedingTemplatesPath,
716
+ 'README.md.template',
717
+ seedingDir,
718
+ 'README.md',
719
+ featureName
720
+ );
721
+ console.log(` ✅ Generated seeding/README.md`);
722
+ }
723
+
724
+ console.log(` ✅ Generated seeding/${featureName}.seed.js`);
725
+ } catch (error) {
726
+ console.error(`❌ Failed to generate seeding files: ${error.message}`);
727
+ throw error;
728
+ }
729
+ }
730
+
731
+ /**
732
+ * Install dependencies
733
+ */
734
+ function installDependencies(projectPath) {
735
+ return new Promise((resolve, reject) => {
736
+ const npm = spawn('npm', ['install'], {
737
+ cwd: projectPath,
738
+ stdio: 'inherit',
739
+ });
740
+
741
+ npm.on('close', (code) => {
742
+ if (code === 0) {
743
+ console.log('✅ Dependencies installed');
744
+ resolve();
745
+ } else {
746
+ reject(new Error(`npm install failed with code ${code}`));
747
+ }
748
+ });
749
+ });
750
+ }
751
+
752
+ /**
753
+ * Check if file exists
754
+ */
755
+ async function fileExists(filePath) {
756
+ try {
757
+ await fs.access(filePath);
758
+ return true;
759
+ } catch {
760
+ return false;
761
+ }
762
+ }
763
+
764
+ /**
765
+ * Validate FBCA structure for feature generation
766
+ */
767
+ async function validateFBCAStructure(projectDir) {
768
+ const requiredPaths = [
769
+ { path: 'src', name: 'src directory' },
770
+ { path: 'src/api', name: 'src/api directory' },
771
+ { path: 'src/api/features', name: 'features directory' },
772
+ { path: 'src/api/lib', name: 'lib directory' },
773
+ { path: 'src/api/server.ts', name: 'server.ts file' },
774
+ { path: 'src/api/lib/api-router.ts', name: 'api-router.ts file' },
775
+ ];
776
+
777
+ const missingPaths = [];
778
+
779
+ for (const required of requiredPaths) {
780
+ const fullPath = path.join(projectDir, required.path);
781
+ if (!(await fileExists(fullPath))) {
782
+ missingPaths.push(required.name);
783
+ }
784
+ }
785
+
786
+ if (missingPaths.length > 0) {
787
+ console.error('❌ Inconsistent FBCA structure detected. Missing:');
788
+ missingPaths.forEach((missing) => console.error(` • ${missing}`));
789
+ console.log('\n💡 To fix this, run: npx appkit generate app');
790
+ console.log(
791
+ ' This will safely add missing AppKit files without overwriting existing ones.'
792
+ );
793
+ process.exit(1);
794
+ }
795
+
796
+ return path.join(projectDir, 'src/api/features');
797
+ }
798
+
799
+ /**
800
+ * Generate user authentication feature with complete setup
801
+ */
802
+ async function generateUserFeature(featuresPath, name, projectDir) {
803
+ const featurePath = path.join(featuresPath, name);
804
+ await fs.mkdir(featurePath, { recursive: true });
805
+
806
+ const templatesPath = path.join(__dirname, `../templates/feature-user`);
807
+
808
+ try {
809
+ // Generate user-specific files from templates
810
+ await generateFromTemplate(
811
+ templatesPath,
812
+ 'user.route.ts.template',
813
+ featurePath,
814
+ 'user.route.ts',
815
+ name
816
+ );
817
+ await generateFromTemplate(
818
+ templatesPath,
819
+ 'user.service.ts.template',
820
+ featurePath,
821
+ 'user.service.ts',
822
+ name
823
+ );
824
+ await generateFromTemplate(
825
+ templatesPath,
826
+ 'user.types.ts.template',
827
+ featurePath,
828
+ 'user.types.ts',
829
+ name
830
+ );
831
+ await generateFromTemplate(
832
+ templatesPath,
833
+ 'user.model.ts.template',
834
+ featurePath,
835
+ 'user.model.ts',
836
+ name
837
+ );
838
+ await generateFromTemplate(
839
+ templatesPath,
840
+ 'user.http.template',
841
+ featurePath,
842
+ 'user.http',
843
+ name
844
+ );
845
+
846
+ console.log(` ✅ Generated user.route.ts`);
847
+ console.log(` ✅ Generated user.service.ts`);
848
+ console.log(` ✅ Generated user.types.ts`);
849
+ console.log(` ✅ Generated user.model.ts`);
850
+ console.log(` ✅ Generated user.http`);
851
+
852
+ // Handle user database integration
853
+ await handleUserDatabaseIntegration(projectDir);
854
+
855
+ // Generate user seeding files
856
+ await generateUserSeedingFiles(templatesPath, projectDir);
857
+
858
+ console.log(`✅ Created user authentication feature with complete setup`);
859
+ } catch (error) {
860
+ console.error(
861
+ `❌ Failed to generate user feature from templates: ${error.message}`
862
+ );
863
+ throw error;
864
+ }
865
+ }
866
+
867
+ /**
868
+ * Handle database integration for user feature
869
+ */
870
+ async function handleUserDatabaseIntegration(projectDir) {
871
+ try {
872
+ console.log(`🗄️ Setting up user database integration...`);
873
+
874
+ // Check if prisma/schema.prisma exists
875
+ const schemaPath = path.join(projectDir, 'prisma/schema.prisma');
876
+ const schemaExists = await fileExists(schemaPath);
877
+
878
+ if (!schemaExists) {
879
+ // Create prisma directory and schema with User model
880
+ await fs.mkdir(path.join(projectDir, 'prisma'), { recursive: true });
881
+
882
+ const userSchema = `// This is your Prisma schema file,
883
+ // learn more about it in the docs: https://pris.ly/d/prisma-schema
884
+
885
+ generator client {
886
+ provider = "prisma-client-js"
887
+ }
888
+
889
+ datasource db {
890
+ provider = "sqlite"
891
+ url = env("DATABASE_URL")
892
+ }
893
+
894
+ model User {
895
+ id Int @id @default(autoincrement())
896
+ email String @unique
897
+ password String
898
+ name String?
899
+ phone String?
900
+ role String @default("user")
901
+ level String @default("basic")
902
+ tenantId String?
903
+ isVerified Boolean @default(false)
904
+ isActive Boolean @default(true)
905
+ lastLogin DateTime?
906
+ resetToken String?
907
+ resetTokenExpiry DateTime?
908
+ createdAt DateTime @default(now())
909
+ updatedAt DateTime @updatedAt
910
+
911
+ @@map("users")
912
+ }
913
+ `;
914
+
915
+ await fs.writeFile(schemaPath, userSchema);
916
+ console.log(`✅ Created prisma/schema.prisma with User model`);
917
+ } else {
918
+ // Schema exists, check if User model already exists
919
+ const schemaContent = await fs.readFile(schemaPath, 'utf8');
920
+ const userModelPattern = /model\s+User\s*\{/i;
921
+
922
+ if (userModelPattern.test(schemaContent)) {
923
+ console.log(`⚠️ User model already exists in schema. Skipping...`);
924
+ } else {
925
+ // Append User model to existing schema
926
+ const userModel = `
927
+ model User {
928
+ id Int @id @default(autoincrement())
929
+ email String @unique
930
+ password String
931
+ name String?
932
+ phone String?
933
+ role String @default("user")
934
+ level String @default("basic")
935
+ tenantId String?
936
+ isVerified Boolean @default(false)
937
+ isActive Boolean @default(true)
938
+ lastLogin DateTime?
939
+ resetToken String?
940
+ resetTokenExpiry DateTime?
941
+ createdAt DateTime @default(now())
942
+ updatedAt DateTime @updatedAt
943
+
944
+ @@map("users")
945
+ }
946
+ `;
947
+
948
+ await fs.appendFile(schemaPath, userModel);
949
+ console.log(`✅ Added User model to existing schema`);
950
+ }
951
+ }
952
+
953
+ // Create or update .env file with DATABASE_URL
954
+ await ensureDatabaseUrl(projectDir);
955
+
956
+ console.log(`📊 User database integration ready!`);
957
+ } catch (error) {
958
+ console.error(
959
+ `❌ Failed to setup user database integration: ${error.message}`
960
+ );
961
+ throw error;
962
+ }
963
+ }
964
+
965
+ /**
966
+ * Generate user seeding files
967
+ */
968
+ async function generateUserSeedingFiles(templatesPath, projectDir) {
969
+ try {
970
+ const seedingDir = path.join(projectDir, 'prisma', 'seeding');
971
+
972
+ // Create seeding directory if it doesn't exist
973
+ await fs.mkdir(seedingDir, { recursive: true });
974
+
975
+ // Generate user seed file
976
+ await generateFromTemplate(
977
+ templatesPath,
978
+ 'user.seed.js.template',
979
+ seedingDir,
980
+ 'user.seed.js',
981
+ 'user'
982
+ );
983
+
984
+ console.log(` ✅ Generated seeding/user.seed.js`);
985
+ } catch (error) {
986
+ console.error(`❌ Failed to generate user seeding files: ${error.message}`);
987
+ throw error;
988
+ }
989
+ }
990
+
991
+ /**
992
+ * Ensure DATABASE_URL, VOILA_AUTH_SECRET, and DEFAULT_USER_PASSWORD exist in .env
993
+ */
994
+ async function ensureDatabaseUrl(projectDir) {
995
+ const envPath = path.join(projectDir, '.env');
996
+
997
+ try {
998
+ // Check if .env exists
999
+ let envContent = '';
1000
+ try {
1001
+ envContent = await fs.readFile(envPath, 'utf8');
1002
+ } catch {
1003
+ // .env doesn't exist, will create it
1004
+ }
1005
+
1006
+ let updated = false;
1007
+
1008
+ // Check if DATABASE_URL already exists
1009
+ if (!envContent.includes('DATABASE_URL=')) {
1010
+ const databaseUrl = '\nDATABASE_URL="file:./dev.db"\n';
1011
+ envContent += databaseUrl;
1012
+ updated = true;
1013
+ console.log(`✅ Added DATABASE_URL to .env`);
1014
+ }
1015
+
1016
+ // Check if VOILA_AUTH_SECRET already exists
1017
+ if (!envContent.includes('VOILA_AUTH_SECRET=')) {
1018
+ const authSecret =
1019
+ 'auth_' +
1020
+ Math.random().toString(36).substring(2, 15) +
1021
+ Math.random().toString(36).substring(2, 15) +
1022
+ Math.random().toString(36).substring(2, 15);
1023
+ const authSecretLine = '\nVOILA_AUTH_SECRET=' + authSecret + '\n';
1024
+ envContent += authSecretLine;
1025
+ updated = true;
1026
+ console.log(`✅ Added VOILA_AUTH_SECRET to .env`);
1027
+ }
1028
+
1029
+ // Check if DEFAULT_USER_PASSWORD already exists
1030
+ if (!envContent.includes('DEFAULT_USER_PASSWORD=')) {
1031
+ const defaultPassword =
1032
+ Math.random().toString(36).substring(2, 8) +
1033
+ Math.random().toString(36).substring(2, 6);
1034
+ const passwordLine = '\nDEFAULT_USER_PASSWORD=' + defaultPassword + '\n';
1035
+ envContent += passwordLine;
1036
+ updated = true;
1037
+ console.log(`✅ Added DEFAULT_USER_PASSWORD to .env`);
1038
+ }
1039
+
1040
+ if (updated) {
1041
+ await fs.writeFile(envPath, envContent, 'utf8');
1042
+ }
1043
+ } catch (error) {
1044
+ console.error(`❌ Failed to setup .env file: ${error.message}`);
1045
+ throw error;
1046
+ }
1047
+ }
1048
+
1049
+ // Legacy export for backward compatibility
1050
+ export const createProject = (name, options) => generate('app', name, options);