@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.
- package/LICENSE +21 -0
- package/README.md +902 -0
- package/bin/appkit.js +71 -0
- package/bin/commands/generate.js +1050 -0
- package/bin/templates/backend/README.md.template +39 -0
- package/bin/templates/backend/api.http.template +0 -0
- package/bin/templates/backend/docs/APPKIT_CLI.md +507 -0
- package/bin/templates/backend/docs/APPKIT_COMMENTS_GUIDELINES.md +61 -0
- package/bin/templates/backend/docs/APPKIT_LLM_GUIDE.md +2539 -0
- package/bin/templates/backend/package.json.template +34 -0
- package/bin/templates/backend/src/api/features/welcome/welcome.http.template +29 -0
- package/bin/templates/backend/src/api/features/welcome/welcome.route.ts.template +36 -0
- package/bin/templates/backend/src/api/features/welcome/welcome.service.ts.template +88 -0
- package/bin/templates/backend/src/api/features/welcome/welcome.types.ts.template +18 -0
- package/bin/templates/backend/src/api/lib/api-router.ts.template +84 -0
- package/bin/templates/backend/src/api/server.ts.template +188 -0
- package/bin/templates/backend/tsconfig.api.json.template +24 -0
- package/bin/templates/backend/tsconfig.json.template +40 -0
- package/bin/templates/feature/feature.http.template +63 -0
- package/bin/templates/feature/feature.route.ts.template +36 -0
- package/bin/templates/feature/feature.service.ts.template +81 -0
- package/bin/templates/feature/feature.types.ts.template +23 -0
- package/bin/templates/feature-db/feature.http.template +63 -0
- package/bin/templates/feature-db/feature.model.ts.template +74 -0
- package/bin/templates/feature-db/feature.route.ts.template +58 -0
- package/bin/templates/feature-db/feature.service.ts.template +231 -0
- package/bin/templates/feature-db/feature.types.ts.template +25 -0
- package/bin/templates/feature-db/schema-addition.prisma.template +9 -0
- package/bin/templates/feature-db/seeding/README.md.template +57 -0
- package/bin/templates/feature-db/seeding/feature.seed.js.template +67 -0
- package/bin/templates/feature-user/schema-addition.prisma.template +19 -0
- package/bin/templates/feature-user/user.http.template +157 -0
- package/bin/templates/feature-user/user.model.ts.template +244 -0
- package/bin/templates/feature-user/user.route.ts.template +379 -0
- package/bin/templates/feature-user/user.seed.js.template +182 -0
- package/bin/templates/feature-user/user.service.ts.template +426 -0
- package/bin/templates/feature-user/user.types.ts.template +127 -0
- package/dist/auth/auth.d.ts +182 -0
- package/dist/auth/auth.d.ts.map +1 -0
- package/dist/auth/auth.js +477 -0
- package/dist/auth/auth.js.map +1 -0
- package/dist/auth/defaults.d.ts +104 -0
- package/dist/auth/defaults.d.ts.map +1 -0
- package/dist/auth/defaults.js +374 -0
- package/dist/auth/defaults.js.map +1 -0
- package/dist/auth/index.d.ts +70 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +94 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/cache/cache.d.ts +118 -0
- package/dist/cache/cache.d.ts.map +1 -0
- package/dist/cache/cache.js +249 -0
- package/dist/cache/cache.js.map +1 -0
- package/dist/cache/defaults.d.ts +63 -0
- package/dist/cache/defaults.d.ts.map +1 -0
- package/dist/cache/defaults.js +193 -0
- package/dist/cache/defaults.js.map +1 -0
- package/dist/cache/index.d.ts +101 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +203 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/strategies/memory.d.ts +138 -0
- package/dist/cache/strategies/memory.d.ts.map +1 -0
- package/dist/cache/strategies/memory.js +348 -0
- package/dist/cache/strategies/memory.js.map +1 -0
- package/dist/cache/strategies/redis.d.ts +105 -0
- package/dist/cache/strategies/redis.d.ts.map +1 -0
- package/dist/cache/strategies/redis.js +318 -0
- package/dist/cache/strategies/redis.js.map +1 -0
- package/dist/config/config.d.ts +62 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +107 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/defaults.d.ts +44 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +217 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/index.d.ts +105 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +163 -0
- package/dist/config/index.js.map +1 -0
- package/dist/database/adapters/mongoose.d.ts +106 -0
- package/dist/database/adapters/mongoose.d.ts.map +1 -0
- package/dist/database/adapters/mongoose.js +480 -0
- package/dist/database/adapters/mongoose.js.map +1 -0
- package/dist/database/adapters/prisma.d.ts +106 -0
- package/dist/database/adapters/prisma.d.ts.map +1 -0
- package/dist/database/adapters/prisma.js +494 -0
- package/dist/database/adapters/prisma.js.map +1 -0
- package/dist/database/defaults.d.ts +87 -0
- package/dist/database/defaults.d.ts.map +1 -0
- package/dist/database/defaults.js +271 -0
- package/dist/database/defaults.js.map +1 -0
- package/dist/database/index.d.ts +137 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +490 -0
- package/dist/database/index.js.map +1 -0
- package/dist/email/defaults.d.ts +100 -0
- package/dist/email/defaults.d.ts.map +1 -0
- package/dist/email/defaults.js +400 -0
- package/dist/email/defaults.js.map +1 -0
- package/dist/email/email.d.ts +139 -0
- package/dist/email/email.d.ts.map +1 -0
- package/dist/email/email.js +316 -0
- package/dist/email/email.js.map +1 -0
- package/dist/email/index.d.ts +176 -0
- package/dist/email/index.d.ts.map +1 -0
- package/dist/email/index.js +251 -0
- package/dist/email/index.js.map +1 -0
- package/dist/email/strategies/console.d.ts +90 -0
- package/dist/email/strategies/console.d.ts.map +1 -0
- package/dist/email/strategies/console.js +268 -0
- package/dist/email/strategies/console.js.map +1 -0
- package/dist/email/strategies/resend.d.ts +84 -0
- package/dist/email/strategies/resend.d.ts.map +1 -0
- package/dist/email/strategies/resend.js +266 -0
- package/dist/email/strategies/resend.js.map +1 -0
- package/dist/email/strategies/smtp.d.ts +77 -0
- package/dist/email/strategies/smtp.d.ts.map +1 -0
- package/dist/email/strategies/smtp.js +286 -0
- package/dist/email/strategies/smtp.js.map +1 -0
- package/dist/error/defaults.d.ts +40 -0
- package/dist/error/defaults.d.ts.map +1 -0
- package/dist/error/defaults.js +75 -0
- package/dist/error/defaults.js.map +1 -0
- package/dist/error/error.d.ts +140 -0
- package/dist/error/error.d.ts.map +1 -0
- package/dist/error/error.js +200 -0
- package/dist/error/error.js.map +1 -0
- package/dist/error/index.d.ts +145 -0
- package/dist/error/index.d.ts.map +1 -0
- package/dist/error/index.js +145 -0
- package/dist/error/index.js.map +1 -0
- package/dist/event/defaults.d.ts +111 -0
- package/dist/event/defaults.d.ts.map +1 -0
- package/dist/event/defaults.js +378 -0
- package/dist/event/defaults.js.map +1 -0
- package/dist/event/event.d.ts +171 -0
- package/dist/event/event.d.ts.map +1 -0
- package/dist/event/event.js +391 -0
- package/dist/event/event.js.map +1 -0
- package/dist/event/index.d.ts +173 -0
- package/dist/event/index.d.ts.map +1 -0
- package/dist/event/index.js +302 -0
- package/dist/event/index.js.map +1 -0
- package/dist/event/strategies/memory.d.ts +122 -0
- package/dist/event/strategies/memory.d.ts.map +1 -0
- package/dist/event/strategies/memory.js +331 -0
- package/dist/event/strategies/memory.js.map +1 -0
- package/dist/event/strategies/redis.d.ts +115 -0
- package/dist/event/strategies/redis.d.ts.map +1 -0
- package/dist/event/strategies/redis.js +434 -0
- package/dist/event/strategies/redis.js.map +1 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +72 -0
- package/dist/index.js.map +1 -0
- package/dist/logger/defaults.d.ts +67 -0
- package/dist/logger/defaults.d.ts.map +1 -0
- package/dist/logger/defaults.js +213 -0
- package/dist/logger/defaults.js.map +1 -0
- package/dist/logger/index.d.ts +84 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/logger/index.js +101 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger/logger.d.ts +165 -0
- package/dist/logger/logger.d.ts.map +1 -0
- package/dist/logger/logger.js +843 -0
- package/dist/logger/logger.js.map +1 -0
- package/dist/logger/transports/console.d.ts +102 -0
- package/dist/logger/transports/console.d.ts.map +1 -0
- package/dist/logger/transports/console.js +276 -0
- package/dist/logger/transports/console.js.map +1 -0
- package/dist/logger/transports/database.d.ts +153 -0
- package/dist/logger/transports/database.d.ts.map +1 -0
- package/dist/logger/transports/database.js +539 -0
- package/dist/logger/transports/database.js.map +1 -0
- package/dist/logger/transports/file.d.ts +146 -0
- package/dist/logger/transports/file.d.ts.map +1 -0
- package/dist/logger/transports/file.js +464 -0
- package/dist/logger/transports/file.js.map +1 -0
- package/dist/logger/transports/http.d.ts +128 -0
- package/dist/logger/transports/http.d.ts.map +1 -0
- package/dist/logger/transports/http.js +401 -0
- package/dist/logger/transports/http.js.map +1 -0
- package/dist/logger/transports/webhook.d.ts +152 -0
- package/dist/logger/transports/webhook.d.ts.map +1 -0
- package/dist/logger/transports/webhook.js +485 -0
- package/dist/logger/transports/webhook.js.map +1 -0
- package/dist/queue/defaults.d.ts +66 -0
- package/dist/queue/defaults.d.ts.map +1 -0
- package/dist/queue/defaults.js +205 -0
- package/dist/queue/defaults.js.map +1 -0
- package/dist/queue/index.d.ts +124 -0
- package/dist/queue/index.d.ts.map +1 -0
- package/dist/queue/index.js +116 -0
- package/dist/queue/index.js.map +1 -0
- package/dist/queue/queue.d.ts +156 -0
- package/dist/queue/queue.d.ts.map +1 -0
- package/dist/queue/queue.js +387 -0
- package/dist/queue/queue.js.map +1 -0
- package/dist/queue/transports/database.d.ts +165 -0
- package/dist/queue/transports/database.d.ts.map +1 -0
- package/dist/queue/transports/database.js +595 -0
- package/dist/queue/transports/database.js.map +1 -0
- package/dist/queue/transports/memory.d.ts +143 -0
- package/dist/queue/transports/memory.d.ts.map +1 -0
- package/dist/queue/transports/memory.js +415 -0
- package/dist/queue/transports/memory.js.map +1 -0
- package/dist/queue/transports/redis.d.ts +203 -0
- package/dist/queue/transports/redis.d.ts.map +1 -0
- package/dist/queue/transports/redis.js +744 -0
- package/dist/queue/transports/redis.js.map +1 -0
- package/dist/security/defaults.d.ts +64 -0
- package/dist/security/defaults.d.ts.map +1 -0
- package/dist/security/defaults.js +159 -0
- package/dist/security/defaults.js.map +1 -0
- package/dist/security/index.d.ts +110 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +160 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/security.d.ts +138 -0
- package/dist/security/security.d.ts.map +1 -0
- package/dist/security/security.js +419 -0
- package/dist/security/security.js.map +1 -0
- package/dist/storage/defaults.d.ts +79 -0
- package/dist/storage/defaults.d.ts.map +1 -0
- package/dist/storage/defaults.js +358 -0
- package/dist/storage/defaults.js.map +1 -0
- package/dist/storage/index.d.ts +153 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +242 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/storage.d.ts +151 -0
- package/dist/storage/storage.d.ts.map +1 -0
- package/dist/storage/storage.js +439 -0
- package/dist/storage/storage.js.map +1 -0
- package/dist/storage/strategies/local.d.ts +117 -0
- package/dist/storage/strategies/local.d.ts.map +1 -0
- package/dist/storage/strategies/local.js +368 -0
- package/dist/storage/strategies/local.js.map +1 -0
- package/dist/storage/strategies/r2.d.ts +130 -0
- package/dist/storage/strategies/r2.d.ts.map +1 -0
- package/dist/storage/strategies/r2.js +470 -0
- package/dist/storage/strategies/r2.js.map +1 -0
- package/dist/storage/strategies/s3.d.ts +121 -0
- package/dist/storage/strategies/s3.d.ts.map +1 -0
- package/dist/storage/strategies/s3.js +461 -0
- package/dist/storage/strategies/s3.js.map +1 -0
- package/dist/util/defaults.d.ts +77 -0
- package/dist/util/defaults.d.ts.map +1 -0
- package/dist/util/defaults.js +193 -0
- package/dist/util/defaults.js.map +1 -0
- package/dist/util/index.d.ts +97 -0
- package/dist/util/index.d.ts.map +1 -0
- package/dist/util/index.js +165 -0
- package/dist/util/index.js.map +1 -0
- package/dist/util/util.d.ts +145 -0
- package/dist/util/util.d.ts.map +1 -0
- package/dist/util/util.js +481 -0
- package/dist/util/util.js.map +1 -0
- 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);
|