@chimerai/cli 0.2.73
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 +293 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +317 -0
- package/dist/commands/add.d.ts +11 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +2126 -0
- package/dist/commands/create.d.ts +12 -0
- package/dist/commands/create.d.ts.map +1 -0
- package/dist/commands/create.js +1703 -0
- package/dist/commands/deploy.d.ts +11 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +219 -0
- package/dist/commands/dev.d.ts +17 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +206 -0
- package/dist/commands/doctor.d.ts +11 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +728 -0
- package/dist/commands/generate.d.ts +19 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +429 -0
- package/dist/commands/init.d.ts +11 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +269 -0
- package/dist/commands/list.d.ts +12 -0
- package/dist/commands/list.d.ts.map +1 -0
- package/dist/commands/list.js +328 -0
- package/dist/commands/migrate.d.ts +14 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +197 -0
- package/dist/commands/plugin.d.ts +10 -0
- package/dist/commands/plugin.d.ts.map +1 -0
- package/dist/commands/plugin.js +239 -0
- package/dist/commands/remove.d.ts +11 -0
- package/dist/commands/remove.d.ts.map +1 -0
- package/dist/commands/remove.js +472 -0
- package/dist/commands/secret.d.ts +12 -0
- package/dist/commands/secret.d.ts.map +1 -0
- package/dist/commands/secret.js +102 -0
- package/dist/commands/setup.d.ts +9 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +788 -0
- package/dist/commands/update.d.ts +14 -0
- package/dist/commands/update.d.ts.map +1 -0
- package/dist/commands/update.js +211 -0
- package/dist/commands/use.d.ts +9 -0
- package/dist/commands/use.d.ts.map +1 -0
- package/dist/commands/use.js +51 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/license.d.ts +55 -0
- package/dist/license.d.ts.map +1 -0
- package/dist/license.js +258 -0
- package/dist/scanner.d.ts +31 -0
- package/dist/scanner.d.ts.map +1 -0
- package/dist/scanner.js +113 -0
- package/dist/schema-manager.d.ts +26 -0
- package/dist/schema-manager.d.ts.map +1 -0
- package/dist/schema-manager.js +132 -0
- package/dist/templates/admin.d.ts +49 -0
- package/dist/templates/admin.d.ts.map +1 -0
- package/dist/templates/admin.js +1358 -0
- package/dist/templates/ai-routes.d.ts +17 -0
- package/dist/templates/ai-routes.d.ts.map +1 -0
- package/dist/templates/ai-routes.js +1130 -0
- package/dist/templates/ai-service-tools.d.ts +22 -0
- package/dist/templates/ai-service-tools.d.ts.map +1 -0
- package/dist/templates/ai-service-tools.js +1424 -0
- package/dist/templates/ai-service.d.ts +66 -0
- package/dist/templates/ai-service.d.ts.map +1 -0
- package/dist/templates/ai-service.js +2202 -0
- package/dist/templates/api-routes.d.ts +108 -0
- package/dist/templates/api-routes.d.ts.map +1 -0
- package/dist/templates/api-routes.js +1219 -0
- package/dist/templates/auth.d.ts +48 -0
- package/dist/templates/auth.d.ts.map +1 -0
- package/dist/templates/auth.js +381 -0
- package/dist/templates/billing.d.ts +44 -0
- package/dist/templates/billing.d.ts.map +1 -0
- package/dist/templates/billing.js +551 -0
- package/dist/templates/chat.d.ts +63 -0
- package/dist/templates/chat.d.ts.map +1 -0
- package/dist/templates/chat.js +1979 -0
- package/dist/templates/components.d.ts +22 -0
- package/dist/templates/components.d.ts.map +1 -0
- package/dist/templates/components.js +672 -0
- package/dist/templates/config.d.ts +6 -0
- package/dist/templates/config.d.ts.map +1 -0
- package/dist/templates/config.js +86 -0
- package/dist/templates/docker.d.ts +25 -0
- package/dist/templates/docker.d.ts.map +1 -0
- package/dist/templates/docker.js +165 -0
- package/dist/templates/gdpr.d.ts +16 -0
- package/dist/templates/gdpr.d.ts.map +1 -0
- package/dist/templates/gdpr.js +259 -0
- package/dist/templates/index.d.ts +77 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +339 -0
- package/dist/templates/layout.d.ts +67 -0
- package/dist/templates/layout.d.ts.map +1 -0
- package/dist/templates/layout.js +670 -0
- package/dist/templates/mfa.d.ts +23 -0
- package/dist/templates/mfa.d.ts.map +1 -0
- package/dist/templates/mfa.js +353 -0
- package/dist/templates/middleware.d.ts +12 -0
- package/dist/templates/middleware.d.ts.map +1 -0
- package/dist/templates/middleware.js +116 -0
- package/dist/templates/prisma.d.ts +35 -0
- package/dist/templates/prisma.d.ts.map +1 -0
- package/dist/templates/prisma.js +724 -0
- package/dist/templates/provider-routes.d.ts +21 -0
- package/dist/templates/provider-routes.d.ts.map +1 -0
- package/dist/templates/provider-routes.js +1203 -0
- package/dist/templates/rag.d.ts +48 -0
- package/dist/templates/rag.d.ts.map +1 -0
- package/dist/templates/rag.js +532 -0
- package/dist/templates/widget.d.ts +64 -0
- package/dist/templates/widget.d.ts.map +1 -0
- package/dist/templates/widget.js +1360 -0
- package/dist/utils/provider-db.d.ts +63 -0
- package/dist/utils/provider-db.d.ts.map +1 -0
- package/dist/utils/provider-db.js +300 -0
- package/dist/utils.d.ts +78 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +330 -0
- package/package.json +60 -0
|
@@ -0,0 +1,1703 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Create Command - Generate a new ChimerAI project with selected features
|
|
4
|
+
* REFACTORED: Uses inline template generators instead of fs.copy()
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
40
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
41
|
+
};
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.createCommand = createCommand;
|
|
44
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
45
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
46
|
+
const ora_1 = __importDefault(require("ora"));
|
|
47
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
48
|
+
const path_1 = __importDefault(require("path"));
|
|
49
|
+
const child_process_1 = require("child_process");
|
|
50
|
+
const templates = __importStar(require("../templates/index.js"));
|
|
51
|
+
const index_js_1 = require("../templates/index.js");
|
|
52
|
+
const utils_js_1 = require("../utils.js");
|
|
53
|
+
const AVAILABLE_FEATURES = [
|
|
54
|
+
{
|
|
55
|
+
name: '🔐 Authentication (NextAuth)',
|
|
56
|
+
value: 'auth',
|
|
57
|
+
checked: true,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: '👥 RBAC System (Users, Roles, Permissions)',
|
|
61
|
+
value: 'rbac',
|
|
62
|
+
checked: true,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: '🔌 Model Providers Management',
|
|
66
|
+
value: 'model-providers',
|
|
67
|
+
checked: true,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: '📝 Prompt Template Management',
|
|
71
|
+
value: 'prompts',
|
|
72
|
+
checked: true,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: '💬 AI Chat Interface',
|
|
76
|
+
value: 'chat',
|
|
77
|
+
checked: false,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: '🔍 RAG / Vector Store (FAISS)',
|
|
81
|
+
value: 'rag',
|
|
82
|
+
checked: false,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: '💳 Billing System (Stripe)',
|
|
86
|
+
value: 'billing',
|
|
87
|
+
checked: false,
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: '📊 API Usage Analytics',
|
|
91
|
+
value: 'analytics',
|
|
92
|
+
checked: true,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: '🎨 Admin Dashboard',
|
|
96
|
+
value: 'admin',
|
|
97
|
+
checked: true,
|
|
98
|
+
},
|
|
99
|
+
];
|
|
100
|
+
async function createCommand(projectName, options) {
|
|
101
|
+
console.log(chalk_1.default.bold.cyan('\n🚀 Create New ChimerAI Project\n'));
|
|
102
|
+
// Validate project name
|
|
103
|
+
if (!projectName) {
|
|
104
|
+
(0, utils_js_1.handleCliError)('Please provide a project name. Usage: chimerai create <project-name>');
|
|
105
|
+
}
|
|
106
|
+
const targetDir = path_1.default.resolve(projectName);
|
|
107
|
+
// Check if directory exists
|
|
108
|
+
if (fs_extra_1.default.existsSync(targetDir)) {
|
|
109
|
+
(0, utils_js_1.handleCliError)(`Directory "${projectName}" already exists`);
|
|
110
|
+
}
|
|
111
|
+
let selectedFeatures;
|
|
112
|
+
if (options.yes) {
|
|
113
|
+
// Use defaults
|
|
114
|
+
selectedFeatures = AVAILABLE_FEATURES.filter((f) => f.checked).map((f) => f.value);
|
|
115
|
+
console.log(chalk_1.default.cyan('Using default feature set...'));
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
// Interactive selection
|
|
119
|
+
const answers = await inquirer_1.default.prompt([
|
|
120
|
+
{
|
|
121
|
+
type: 'checkbox',
|
|
122
|
+
name: 'features',
|
|
123
|
+
message: 'Select features to include:',
|
|
124
|
+
choices: AVAILABLE_FEATURES,
|
|
125
|
+
pageSize: 15,
|
|
126
|
+
},
|
|
127
|
+
]);
|
|
128
|
+
selectedFeatures = answers.features;
|
|
129
|
+
}
|
|
130
|
+
if (selectedFeatures.length === 0) {
|
|
131
|
+
(0, utils_js_1.handleCliError)('No features selected');
|
|
132
|
+
}
|
|
133
|
+
console.log(chalk_1.default.bold.green('\n✨ Creating project with features:'));
|
|
134
|
+
selectedFeatures.forEach((f) => {
|
|
135
|
+
const feature = AVAILABLE_FEATURES.find((feat) => feat.value === f);
|
|
136
|
+
console.log(chalk_1.default.white(` ${feature?.name}`));
|
|
137
|
+
});
|
|
138
|
+
console.log(chalk_1.default.white(` 📦 Database: ${options.sqlite ? 'SQLite (no Docker needed)' : 'PostgreSQL'}`));
|
|
139
|
+
console.log('');
|
|
140
|
+
// Create project
|
|
141
|
+
await createProject(targetDir, projectName, selectedFeatures, options.sqlite);
|
|
142
|
+
console.log(chalk_1.default.bold.green('\n✅ Project created successfully!\n'));
|
|
143
|
+
// Auto-install dependencies if --install flag is set
|
|
144
|
+
if (options.install) {
|
|
145
|
+
console.log(chalk_1.default.cyan('\n🚀 Running installation script...\n'));
|
|
146
|
+
try {
|
|
147
|
+
// Detect OS and run appropriate install script
|
|
148
|
+
const isWindows = process.platform === 'win32';
|
|
149
|
+
const installScript = isWindows ? 'install.bat' : 'bash install.sh';
|
|
150
|
+
(0, child_process_1.execSync)(installScript, { cwd: targetDir, stdio: 'inherit' });
|
|
151
|
+
console.log(chalk_1.default.bold.green('\n🎉 Full installation completed!\n'));
|
|
152
|
+
console.log(chalk_1.default.cyan('You can now run:'));
|
|
153
|
+
console.log(chalk_1.default.white(` cd ${projectName}`));
|
|
154
|
+
console.log(chalk_1.default.white(' npm run dev'));
|
|
155
|
+
console.log(chalk_1.default.gray('\n Server will run on http://localhost:3001\n'));
|
|
156
|
+
console.log(chalk_1.default.yellow('Login with:'));
|
|
157
|
+
console.log(chalk_1.default.white(' Email: admin@example.com'));
|
|
158
|
+
console.log(chalk_1.default.white(' Password: admin123\n'));
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
console.log(chalk_1.default.red('\n❌ Installation failed'));
|
|
162
|
+
console.log(chalk_1.default.yellow(`You can complete setup manually with: ${process.platform === 'win32' ? '.\\install.bat' : './install.sh'}\n`));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Show next steps
|
|
166
|
+
console.log(chalk_1.default.cyan('Next steps:'));
|
|
167
|
+
console.log(chalk_1.default.white(` cd ${projectName}`));
|
|
168
|
+
if (!options.install) {
|
|
169
|
+
console.log(chalk_1.default.bold.white('\n Quick start:'));
|
|
170
|
+
console.log(chalk_1.default.white(' ./install.bat (Windows)'));
|
|
171
|
+
console.log(chalk_1.default.white(' ./install.sh (Linux/macOS)'));
|
|
172
|
+
console.log(chalk_1.default.bold.white('\n Or manually:'));
|
|
173
|
+
console.log(chalk_1.default.white(' npm install'));
|
|
174
|
+
console.log(chalk_1.default.white(' docker-compose up -d'));
|
|
175
|
+
console.log(chalk_1.default.white(' npm run db:push'));
|
|
176
|
+
console.log(chalk_1.default.white(' npm run db:seed'));
|
|
177
|
+
console.log(chalk_1.default.white(' npm run dev'));
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
console.log(chalk_1.default.white(' docker-compose up -d'));
|
|
181
|
+
console.log(chalk_1.default.white(' npm run db:push'));
|
|
182
|
+
console.log(chalk_1.default.white(' npm run db:seed'));
|
|
183
|
+
console.log(chalk_1.default.white(' npm run dev'));
|
|
184
|
+
}
|
|
185
|
+
console.log(chalk_1.default.gray('\n Server will run on http://localhost:3001\n'));
|
|
186
|
+
}
|
|
187
|
+
async function createProject(targetDir, projectName, features, sqlite) {
|
|
188
|
+
const spinner = (0, ora_1.default)('Creating project structure...').start();
|
|
189
|
+
try {
|
|
190
|
+
// Create base directory
|
|
191
|
+
await fs_extra_1.default.ensureDir(targetDir);
|
|
192
|
+
// 1. Create base Next.js structure
|
|
193
|
+
spinner.text = 'Setting up Next.js base...';
|
|
194
|
+
await createBaseStructure(targetDir, projectName, features);
|
|
195
|
+
// 2. Create package.json
|
|
196
|
+
spinner.text = 'Generating package.json...';
|
|
197
|
+
await createPackageJson(targetDir, projectName, features);
|
|
198
|
+
// 3. Create TypeScript config
|
|
199
|
+
spinner.text = 'Setting up TypeScript...';
|
|
200
|
+
await createTsConfig(targetDir);
|
|
201
|
+
// 4. Create Prisma schema
|
|
202
|
+
spinner.text = 'Setting up database schema...';
|
|
203
|
+
await createPrismaSchema(targetDir, features, sqlite);
|
|
204
|
+
// 5. Create environment files
|
|
205
|
+
spinner.text = 'Creating environment files...';
|
|
206
|
+
await createEnvFiles(targetDir, features, sqlite);
|
|
207
|
+
// 6. Copy feature-specific files
|
|
208
|
+
spinner.text = 'Adding selected features...';
|
|
209
|
+
await copyFeatureFiles(targetDir, features);
|
|
210
|
+
// 7. Create seed script
|
|
211
|
+
spinner.text = 'Creating database seed...';
|
|
212
|
+
await createSeedScript(targetDir, features, sqlite);
|
|
213
|
+
// 8. Create Docker Compose
|
|
214
|
+
spinner.text = 'Setting up Docker...';
|
|
215
|
+
await createDockerCompose(targetDir);
|
|
216
|
+
// 9. Create install scripts
|
|
217
|
+
spinner.text = 'Creating install scripts...';
|
|
218
|
+
await createInstallScripts(targetDir);
|
|
219
|
+
// 10. Create README
|
|
220
|
+
spinner.text = 'Creating documentation...';
|
|
221
|
+
await createReadme(targetDir, projectName, features);
|
|
222
|
+
// 11. Create .chimerai marker file
|
|
223
|
+
spinner.text = 'Registering project...';
|
|
224
|
+
const chimeraiMarker = {
|
|
225
|
+
version: '1.0.0',
|
|
226
|
+
name: projectName,
|
|
227
|
+
created: new Date().toISOString().split('T')[0],
|
|
228
|
+
};
|
|
229
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, '.chimerai'), JSON.stringify(chimeraiMarker, null, 2));
|
|
230
|
+
// Register in global registry
|
|
231
|
+
(0, utils_js_1.registerProject)(projectName, targetDir);
|
|
232
|
+
spinner.succeed(chalk_1.default.green('Project structure created'));
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
spinner.fail(chalk_1.default.red('Failed to create project'));
|
|
236
|
+
console.error(error.message);
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
async function createBaseStructure(targetDir, projectName, features) {
|
|
241
|
+
// Create base directories
|
|
242
|
+
const dirs = [
|
|
243
|
+
'app',
|
|
244
|
+
'app/api',
|
|
245
|
+
'components',
|
|
246
|
+
'components/ui',
|
|
247
|
+
'lib',
|
|
248
|
+
'prisma',
|
|
249
|
+
'public',
|
|
250
|
+
'styles',
|
|
251
|
+
];
|
|
252
|
+
// Add feature-specific directories
|
|
253
|
+
if (features.includes('admin')) {
|
|
254
|
+
dirs.push('app/admin', 'app/admin/settings', 'app/admin/logs');
|
|
255
|
+
}
|
|
256
|
+
if (features.includes('chat')) {
|
|
257
|
+
dirs.push('app/(app)', 'app/(app)/chat', 'components/chat', 'app/api/conversations/[id]');
|
|
258
|
+
}
|
|
259
|
+
if (features.includes('auth')) {
|
|
260
|
+
dirs.push('app/(auth)', 'app/(auth)/login', 'app/api/auth/[...nextauth]');
|
|
261
|
+
}
|
|
262
|
+
if (features.includes('rbac')) {
|
|
263
|
+
dirs.push('app/admin/users', 'app/admin/roles');
|
|
264
|
+
}
|
|
265
|
+
if (features.includes('model-providers')) {
|
|
266
|
+
dirs.push('app/dashboard/providers');
|
|
267
|
+
}
|
|
268
|
+
if (features.includes('prompts')) {
|
|
269
|
+
dirs.push('app/dashboard/prompts', 'app/api/prompts', 'app/api/prompts/[id]');
|
|
270
|
+
}
|
|
271
|
+
for (const dir of dirs) {
|
|
272
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, dir));
|
|
273
|
+
}
|
|
274
|
+
// Create app/layout.tsx using template generator
|
|
275
|
+
const layoutContent = features.includes('auth')
|
|
276
|
+
? templates.generateAppLayoutWithAuth(projectName)
|
|
277
|
+
: templates.generateAppLayout(projectName);
|
|
278
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/layout.tsx'), layoutContent);
|
|
279
|
+
// Create app/page.tsx using template generator
|
|
280
|
+
const pageContent = templates.generateAppPage();
|
|
281
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/page.tsx'), pageContent);
|
|
282
|
+
// Create globals.css using template generator
|
|
283
|
+
const globalsCss = templates.generateGlobalsCss();
|
|
284
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/globals.css'), globalsCss);
|
|
285
|
+
}
|
|
286
|
+
async function createPackageJson(targetDir, projectName, features) {
|
|
287
|
+
const dependencies = {
|
|
288
|
+
next: '^15.0.0',
|
|
289
|
+
react: '^18.3.1',
|
|
290
|
+
'react-dom': '^18.3.1',
|
|
291
|
+
'@prisma/client': '^5.22.0',
|
|
292
|
+
};
|
|
293
|
+
const devDependencies = {
|
|
294
|
+
'@types/node': '^20',
|
|
295
|
+
'@types/react': '^18',
|
|
296
|
+
'@types/react-dom': '^18',
|
|
297
|
+
typescript: '^5',
|
|
298
|
+
tailwindcss: '^3.4.0',
|
|
299
|
+
postcss: '^8',
|
|
300
|
+
autoprefixer: '^10',
|
|
301
|
+
prisma: '^5.22.0',
|
|
302
|
+
tsx: '^4.20.6',
|
|
303
|
+
};
|
|
304
|
+
// Add feature-specific dependencies
|
|
305
|
+
if (features.includes('auth')) {
|
|
306
|
+
dependencies['next-auth'] = '^4.24.10';
|
|
307
|
+
dependencies['@auth/prisma-adapter'] = '^2.11.1';
|
|
308
|
+
dependencies['bcryptjs'] = '^2.4.3';
|
|
309
|
+
dependencies['next-themes'] = '^0.4.4';
|
|
310
|
+
dependencies['sonner'] = '^1.7.0';
|
|
311
|
+
devDependencies['@types/bcryptjs'] = '^2.4.6';
|
|
312
|
+
}
|
|
313
|
+
// Note: ChimerAI workspace packages are not yet published to npm
|
|
314
|
+
// For standalone projects, these features will need manual implementation
|
|
315
|
+
// or you can use the complete starter kit with 'chimerai init'
|
|
316
|
+
if (features.includes('rbac')) {
|
|
317
|
+
// dependencies['@chimerai/rbac-core'] = 'workspace:*'; // Not available standalone yet
|
|
318
|
+
}
|
|
319
|
+
if (features.includes('model-providers')) {
|
|
320
|
+
// dependencies['@chimerai/model-providers'] = 'workspace:*'; // Not available standalone yet
|
|
321
|
+
}
|
|
322
|
+
if (features.includes('admin')) {
|
|
323
|
+
// dependencies['@chimerai/admin-ui'] = 'workspace:*'; // Not available standalone yet
|
|
324
|
+
}
|
|
325
|
+
if (features.includes('chat')) {
|
|
326
|
+
dependencies['react-markdown'] = '^9.0.0';
|
|
327
|
+
dependencies['remark-gfm'] = '^4.0.0';
|
|
328
|
+
}
|
|
329
|
+
if (features.includes('billing')) {
|
|
330
|
+
// dependencies['@chimerai/billing'] = 'workspace:*'; // Not available standalone yet
|
|
331
|
+
dependencies['stripe'] = '^14.0.0';
|
|
332
|
+
}
|
|
333
|
+
// UI dependencies
|
|
334
|
+
dependencies['@radix-ui/react-dialog'] = '^1.0.5';
|
|
335
|
+
dependencies['@radix-ui/react-dropdown-menu'] = '^2.0.6';
|
|
336
|
+
dependencies['@radix-ui/react-slot'] = '^1.2.4';
|
|
337
|
+
dependencies['class-variance-authority'] = '^0.7.0';
|
|
338
|
+
dependencies['clsx'] = '^2.0.0';
|
|
339
|
+
dependencies['tailwind-merge'] = '^2.2.0';
|
|
340
|
+
dependencies['lucide-react'] = '^0.294.0';
|
|
341
|
+
const packageJson = {
|
|
342
|
+
name: projectName.toLowerCase().replace(/\s+/g, '-'),
|
|
343
|
+
version: '0.1.0',
|
|
344
|
+
private: true,
|
|
345
|
+
scripts: {
|
|
346
|
+
dev: 'next dev -p 3001',
|
|
347
|
+
build: 'prisma generate && next build',
|
|
348
|
+
start: 'next start',
|
|
349
|
+
lint: 'next lint',
|
|
350
|
+
postinstall: 'prisma generate',
|
|
351
|
+
'db:generate': 'prisma generate',
|
|
352
|
+
'db:push': 'prisma db push',
|
|
353
|
+
'db:seed': 'tsx prisma/seed.ts',
|
|
354
|
+
'db:studio': 'prisma studio',
|
|
355
|
+
},
|
|
356
|
+
dependencies,
|
|
357
|
+
devDependencies,
|
|
358
|
+
prisma: {
|
|
359
|
+
seed: 'tsx prisma/seed.ts',
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'package.json'), JSON.stringify(packageJson, null, 2));
|
|
363
|
+
}
|
|
364
|
+
async function createTsConfig(targetDir) {
|
|
365
|
+
// Create next.config.js using template generator
|
|
366
|
+
const nextConfig = templates.generateNextConfig();
|
|
367
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'next.config.js'), nextConfig);
|
|
368
|
+
// Create tsconfig.json using template generator
|
|
369
|
+
const tsConfig = templates.generateTsConfig();
|
|
370
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'tsconfig.json'), tsConfig);
|
|
371
|
+
// Create tailwind.config.js using template generator
|
|
372
|
+
const tailwindConfig = templates.generateTailwindConfig();
|
|
373
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'tailwind.config.js'), tailwindConfig);
|
|
374
|
+
// Create postcss.config.js using template generator
|
|
375
|
+
const postcssConfig = templates.generatePostcssConfig();
|
|
376
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'postcss.config.js'), postcssConfig);
|
|
377
|
+
}
|
|
378
|
+
async function createPrismaSchema(targetDir, features, sqlite) {
|
|
379
|
+
const dbProvider = sqlite ? 'sqlite' : 'postgresql';
|
|
380
|
+
let schemaContent = `generator client {
|
|
381
|
+
provider = "prisma-client-js"
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
datasource db {
|
|
385
|
+
provider = "${dbProvider}"
|
|
386
|
+
url = env("DATABASE_URL")
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
`;
|
|
390
|
+
// Add User model if auth is included
|
|
391
|
+
if (features.includes('auth')) {
|
|
392
|
+
schemaContent += `model User {
|
|
393
|
+
id String @id @default(cuid())
|
|
394
|
+
name String?
|
|
395
|
+
email String? @unique
|
|
396
|
+
emailVerified DateTime?
|
|
397
|
+
image String?
|
|
398
|
+
password String?
|
|
399
|
+
createdAt DateTime @default(now())
|
|
400
|
+
updatedAt DateTime @updatedAt
|
|
401
|
+
|
|
402
|
+
accounts Account[]
|
|
403
|
+
sessions Session[]
|
|
404
|
+
`;
|
|
405
|
+
if (features.includes('rbac')) {
|
|
406
|
+
schemaContent += ` roles UserRole[]
|
|
407
|
+
`;
|
|
408
|
+
}
|
|
409
|
+
// Provider relations (ALWAYS included)
|
|
410
|
+
schemaContent += ` providers Provider[] @relation("CreatedProviders")
|
|
411
|
+
apiUsage ApiUsage[]
|
|
412
|
+
apiKeys ApiKey[]
|
|
413
|
+
`;
|
|
414
|
+
if (features.includes('rbac')) {
|
|
415
|
+
schemaContent += ` modelAccess ModelAccess[]
|
|
416
|
+
auditLogs AuditLog[]
|
|
417
|
+
`;
|
|
418
|
+
}
|
|
419
|
+
if (features.includes('chat')) {
|
|
420
|
+
schemaContent += ` conversations Conversation[]
|
|
421
|
+
`;
|
|
422
|
+
}
|
|
423
|
+
schemaContent += `}
|
|
424
|
+
|
|
425
|
+
model Account {
|
|
426
|
+
id String @id @default(cuid())
|
|
427
|
+
userId String
|
|
428
|
+
type String
|
|
429
|
+
provider String
|
|
430
|
+
providerAccountId String
|
|
431
|
+
refresh_token String? @db.Text
|
|
432
|
+
access_token String? @db.Text
|
|
433
|
+
expires_at Int?
|
|
434
|
+
token_type String?
|
|
435
|
+
scope String?
|
|
436
|
+
id_token String? @db.Text
|
|
437
|
+
session_state String?
|
|
438
|
+
|
|
439
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
440
|
+
|
|
441
|
+
@@unique([provider, providerAccountId])
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
model Session {
|
|
445
|
+
id String @id @default(cuid())
|
|
446
|
+
sessionToken String @unique
|
|
447
|
+
userId String
|
|
448
|
+
expires DateTime
|
|
449
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
model VerificationToken {
|
|
453
|
+
identifier String
|
|
454
|
+
token String @unique
|
|
455
|
+
expires DateTime
|
|
456
|
+
|
|
457
|
+
@@unique([identifier, token])
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
model ApiKey {
|
|
461
|
+
id String @id @default(cuid())
|
|
462
|
+
name String
|
|
463
|
+
keyHash String @unique
|
|
464
|
+
userId String
|
|
465
|
+
scopes String[] @default([])
|
|
466
|
+
revoked Boolean @default(false)
|
|
467
|
+
lastUsedAt DateTime?
|
|
468
|
+
expiresAt DateTime?
|
|
469
|
+
createdAt DateTime @default(now())
|
|
470
|
+
updatedAt DateTime @updatedAt
|
|
471
|
+
|
|
472
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
473
|
+
|
|
474
|
+
@@index([userId])
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
`;
|
|
478
|
+
}
|
|
479
|
+
// Add RBAC models
|
|
480
|
+
if (features.includes('rbac')) {
|
|
481
|
+
schemaContent += `model Role {
|
|
482
|
+
id String @id @default(cuid())
|
|
483
|
+
name String @unique
|
|
484
|
+
description String?
|
|
485
|
+
permissions String[]
|
|
486
|
+
createdAt DateTime @default(now())
|
|
487
|
+
updatedAt DateTime @updatedAt
|
|
488
|
+
|
|
489
|
+
users UserRole[]
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
model UserRole {
|
|
493
|
+
userId String
|
|
494
|
+
roleId String
|
|
495
|
+
|
|
496
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
497
|
+
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
|
|
498
|
+
|
|
499
|
+
@@id([userId, roleId])
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
`;
|
|
503
|
+
}
|
|
504
|
+
// ── PROVIDER MODELS (ALWAYS included — core infrastructure) ──────────
|
|
505
|
+
schemaContent += `// === Provider Management (Core Infrastructure) ===
|
|
506
|
+
|
|
507
|
+
model Provider {
|
|
508
|
+
id String @id @default(cuid())
|
|
509
|
+
name String
|
|
510
|
+
type String // "openai", "anthropic", "ollama", "google", "custom"
|
|
511
|
+
description String?
|
|
512
|
+
baseUrl String?
|
|
513
|
+
apiKey String? @db.Text // AES-256-GCM encrypted, null for keyless providers (e.g. Ollama)
|
|
514
|
+
config Json @default("{}")
|
|
515
|
+
status String @default("active")
|
|
516
|
+
isDefault Boolean @default(false)
|
|
517
|
+
priority Int @default(0)
|
|
518
|
+
tags String[]
|
|
519
|
+
createdAt DateTime @default(now())
|
|
520
|
+
updatedAt DateTime @updatedAt
|
|
521
|
+
createdBy String?
|
|
522
|
+
|
|
523
|
+
models Model[]
|
|
524
|
+
health ProviderHealth?
|
|
525
|
+
apiUsage ApiUsage[]
|
|
526
|
+
${features.includes('chat') ? ' conversations Conversation[]\n' : ''}${features.includes('auth') ? ' creator User? @relation("CreatedProviders", fields: [createdBy], references: [id])\n' : ''}
|
|
527
|
+
@@index([type])
|
|
528
|
+
@@index([status])
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
model Model {
|
|
532
|
+
id String @id @default(cuid())
|
|
533
|
+
providerId String
|
|
534
|
+
modelId String // e.g. "gpt-4", "claude-3-sonnet"
|
|
535
|
+
name String
|
|
536
|
+
description String?
|
|
537
|
+
capabilities String[] // ["chat", "embedding", "image", "vision"]
|
|
538
|
+
contextWindow Int @default(4096)
|
|
539
|
+
maxOutputTokens Int?
|
|
540
|
+
inputCost Float @default(0) // $ per 1M tokens
|
|
541
|
+
outputCost Float @default(0)
|
|
542
|
+
isAvailable Boolean @default(true)
|
|
543
|
+
isDeprecated Boolean @default(false)
|
|
544
|
+
|
|
545
|
+
provider Provider @relation(fields: [providerId], references: [id], onDelete: Cascade)
|
|
546
|
+
|
|
547
|
+
@@unique([providerId, modelId])
|
|
548
|
+
@@index([providerId])
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
model ProviderHealth {
|
|
552
|
+
id String @id @default(cuid())
|
|
553
|
+
providerId String @unique
|
|
554
|
+
status String @default("unknown") // "healthy", "degraded", "unhealthy"
|
|
555
|
+
responseTime Int? // ms
|
|
556
|
+
lastCheck DateTime @default(now())
|
|
557
|
+
errorMessage String?
|
|
558
|
+
modelsAvailable Int @default(0)
|
|
559
|
+
chatAvailable Boolean @default(false)
|
|
560
|
+
embeddingAvailable Boolean @default(false)
|
|
561
|
+
apiKeyValid Boolean @default(false)
|
|
562
|
+
|
|
563
|
+
provider Provider @relation(fields: [providerId], references: [id], onDelete: Cascade)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
model ApiUsage {
|
|
567
|
+
id String @id @default(cuid())
|
|
568
|
+
userId String
|
|
569
|
+
providerId String?
|
|
570
|
+
model String
|
|
571
|
+
endpoint String
|
|
572
|
+
promptTokens Int @default(0)
|
|
573
|
+
completionTokens Int @default(0)
|
|
574
|
+
totalTokens Int @default(0)
|
|
575
|
+
tokensUsed Int @default(0)
|
|
576
|
+
creditsUsed Int @default(0)
|
|
577
|
+
cost Float @default(0)
|
|
578
|
+
success Boolean @default(true)
|
|
579
|
+
errorMessage String?
|
|
580
|
+
responseTime Int @default(0) // ms
|
|
581
|
+
createdAt DateTime @default(now())
|
|
582
|
+
|
|
583
|
+
${features.includes('auth') ? ' user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n' : ''} provider Provider? @relation(fields: [providerId], references: [id], onDelete: SetNull)
|
|
584
|
+
|
|
585
|
+
@@index([userId])
|
|
586
|
+
@@index([providerId])
|
|
587
|
+
@@index([createdAt])
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
`;
|
|
591
|
+
// Add ModelAccess when auth + rbac are enabled (for requireModelPermission)
|
|
592
|
+
if (features.includes('auth') && features.includes('rbac')) {
|
|
593
|
+
schemaContent += `model ModelAccess {
|
|
594
|
+
id String @id @default(cuid())
|
|
595
|
+
userId String
|
|
596
|
+
modelId String
|
|
597
|
+
granted Boolean @default(true)
|
|
598
|
+
createdAt DateTime @default(now())
|
|
599
|
+
updatedAt DateTime @updatedAt
|
|
600
|
+
|
|
601
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
602
|
+
|
|
603
|
+
@@unique([userId, modelId])
|
|
604
|
+
@@index([userId])
|
|
605
|
+
@@index([modelId])
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
model AuditLog {
|
|
609
|
+
id String @id @default(cuid())
|
|
610
|
+
action String
|
|
611
|
+
userId String
|
|
612
|
+
targetType String?
|
|
613
|
+
targetId String?
|
|
614
|
+
metadata Json?
|
|
615
|
+
ipAddress String?
|
|
616
|
+
createdAt DateTime @default(now())
|
|
617
|
+
|
|
618
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
619
|
+
|
|
620
|
+
@@index([userId])
|
|
621
|
+
@@index([action])
|
|
622
|
+
@@index([createdAt])
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
`;
|
|
626
|
+
}
|
|
627
|
+
// Add Prompt Templates
|
|
628
|
+
if (features.includes('prompts')) {
|
|
629
|
+
schemaContent += `model PromptTemplate {
|
|
630
|
+
id String @id @default(cuid())
|
|
631
|
+
name String @unique
|
|
632
|
+
category String
|
|
633
|
+
description String?
|
|
634
|
+
content String @db.Text
|
|
635
|
+
variables String[]
|
|
636
|
+
language String @default("en")
|
|
637
|
+
version Int @default(1)
|
|
638
|
+
isActive Boolean @default(true)
|
|
639
|
+
isDefault Boolean @default(false)
|
|
640
|
+
tags String[]
|
|
641
|
+
metadata Json?
|
|
642
|
+
createdBy String?
|
|
643
|
+
createdAt DateTime @default(now())
|
|
644
|
+
updatedAt DateTime @updatedAt
|
|
645
|
+
|
|
646
|
+
@@index([category])
|
|
647
|
+
@@index([isActive])
|
|
648
|
+
@@index([isDefault])
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
`;
|
|
652
|
+
}
|
|
653
|
+
// Add Conversation & Message models for chat feature
|
|
654
|
+
if (features.includes('chat')) {
|
|
655
|
+
schemaContent += `model Conversation {
|
|
656
|
+
id String @id @default(cuid())
|
|
657
|
+
userId String
|
|
658
|
+
title String @default("New Chat")
|
|
659
|
+
model String?
|
|
660
|
+
providerId String?
|
|
661
|
+
metadata Json?
|
|
662
|
+
archived Boolean @default(false)
|
|
663
|
+
createdAt DateTime @default(now())
|
|
664
|
+
updatedAt DateTime @updatedAt
|
|
665
|
+
|
|
666
|
+
${features.includes('auth') ? ' user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n' : ''} provider Provider? @relation(fields: [providerId], references: [id], onDelete: SetNull)
|
|
667
|
+
messages Message[]
|
|
668
|
+
|
|
669
|
+
@@index([userId])
|
|
670
|
+
@@index([archived])
|
|
671
|
+
@@index([providerId])
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
model Message {
|
|
675
|
+
id String @id @default(cuid())
|
|
676
|
+
conversationId String
|
|
677
|
+
role String
|
|
678
|
+
content String @db.Text
|
|
679
|
+
model String?
|
|
680
|
+
tokens Int?
|
|
681
|
+
createdAt DateTime @default(now())
|
|
682
|
+
|
|
683
|
+
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
|
684
|
+
|
|
685
|
+
@@index([conversationId])
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
`;
|
|
689
|
+
}
|
|
690
|
+
// SystemSetting is always included (used by admin settings + app-settings API)
|
|
691
|
+
schemaContent += `model SystemSetting {
|
|
692
|
+
key String @id
|
|
693
|
+
value String @db.Text
|
|
694
|
+
updatedAt DateTime @updatedAt
|
|
695
|
+
}
|
|
696
|
+
`;
|
|
697
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'prisma/schema.prisma'), schemaContent);
|
|
698
|
+
// Apply SQLite compatibility transform if needed
|
|
699
|
+
if (sqlite) {
|
|
700
|
+
const schemaPath = path_1.default.join(targetDir, 'prisma/schema.prisma');
|
|
701
|
+
const schema = await fs_extra_1.default.readFile(schemaPath, 'utf-8');
|
|
702
|
+
const transformed = transformSchemaForSqlite(schema);
|
|
703
|
+
if (transformed !== schema) {
|
|
704
|
+
await fs_extra_1.default.writeFile(schemaPath, transformed);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Transform PostgreSQL-specific types to SQLite-compatible equivalents.
|
|
710
|
+
* - Removes @db.Text (SQLite strings are unbounded)
|
|
711
|
+
* - Converts Json/Json? → String/String?
|
|
712
|
+
* - Converts String[] → String (arrays not supported in SQLite)
|
|
713
|
+
* - Fixes array defaults: @default(["chat"]) → @default("chat")
|
|
714
|
+
*/
|
|
715
|
+
function transformSchemaForSqlite(schema) {
|
|
716
|
+
let s = schema;
|
|
717
|
+
// Remove @db.Text (SQLite strings are unbounded)
|
|
718
|
+
s = s.replace(/\s+@db\.Text/g, '');
|
|
719
|
+
// Convert Json/Json? → String/String? with proper defaults
|
|
720
|
+
s = s.replace(/(\s+)Json(\??)(\s+)/g, '$1String$2$3');
|
|
721
|
+
// Convert array defaults BEFORE replacing String[] → String
|
|
722
|
+
// e.g. @default(["chat","vision"]) → @default("chat,vision")
|
|
723
|
+
// @default([]) → @default("[]")
|
|
724
|
+
s = s.replace(/@default\(\[([^\]]*)\]\)/g, (_match, inner) => {
|
|
725
|
+
const trimmed = inner.trim();
|
|
726
|
+
if (!trimmed)
|
|
727
|
+
return '@default("[]")'; // empty array → "[]"
|
|
728
|
+
const items = trimmed
|
|
729
|
+
.replace(/"/g, '')
|
|
730
|
+
.split(',')
|
|
731
|
+
.map((x) => x.trim())
|
|
732
|
+
.join(',');
|
|
733
|
+
return `@default("${items}")`;
|
|
734
|
+
});
|
|
735
|
+
// Convert String[] → String, adding @default("[]") if none exists
|
|
736
|
+
s = s.replace(/^(\s+\w+\s+)String\[\](.*)$/gm, (_match, prefix, rest) => {
|
|
737
|
+
const hasDefault = /@default\(/.test(rest);
|
|
738
|
+
return hasDefault ? `${prefix}String${rest}` : `${prefix}String${rest} @default("[]")`;
|
|
739
|
+
});
|
|
740
|
+
return s;
|
|
741
|
+
}
|
|
742
|
+
async function createEnvFiles(targetDir, features, sqlite) {
|
|
743
|
+
// Generate random 32-byte hex encryption key
|
|
744
|
+
const encryptionKey = Array.from({ length: 64 }, () => '0123456789abcdef'.charAt(Math.floor(Math.random() * 16))).join('');
|
|
745
|
+
// Generate random internal API token
|
|
746
|
+
const internalToken = 'chimerai_internal_' +
|
|
747
|
+
Array.from({ length: 64 }, () => '0123456789abcdef'.charAt(Math.floor(Math.random() * 16))).join('');
|
|
748
|
+
// Generate a random NextAuth secret (32 bytes hex = 64 chars)
|
|
749
|
+
const nextAuthSecret = Array.from({ length: 64 }, () => '0123456789abcdef'.charAt(Math.floor(Math.random() * 16))).join('');
|
|
750
|
+
const dbUrl = sqlite ? 'file:./dev.db' : 'postgresql://postgres:postgres@localhost:5432/myapp_db';
|
|
751
|
+
let envContent = `# Database${sqlite ? ' (SQLite — no Docker needed)' : ''}
|
|
752
|
+
DATABASE_URL=${dbUrl}
|
|
753
|
+
|
|
754
|
+
`;
|
|
755
|
+
if (features.includes('auth')) {
|
|
756
|
+
envContent += `# Authentication
|
|
757
|
+
NEXTAUTH_SECRET=${nextAuthSecret}
|
|
758
|
+
NEXTAUTH_URL=http://localhost:3000
|
|
759
|
+
|
|
760
|
+
`;
|
|
761
|
+
}
|
|
762
|
+
// ── Provider Infrastructure (ALWAYS included — core) ──────────
|
|
763
|
+
envContent += `# Provider Encryption (AES-256-GCM for API key storage)
|
|
764
|
+
PROVIDER_ENCRYPTION_KEY=${encryptionKey}
|
|
765
|
+
|
|
766
|
+
# Internal API Token (AI-Service ↔ Frontend communication)
|
|
767
|
+
INTERNAL_API_TOKEN=${internalToken}
|
|
768
|
+
|
|
769
|
+
# AI Service URL
|
|
770
|
+
AI_SERVICE_URL=http://localhost:8002
|
|
771
|
+
|
|
772
|
+
# AI Providers (Optional — can also be added via Provider Management UI)
|
|
773
|
+
OPENAI_API_KEY=
|
|
774
|
+
ANTHROPIC_API_KEY=
|
|
775
|
+
AZURE_OPENAI_API_KEY=
|
|
776
|
+
AZURE_OPENAI_ENDPOINT=
|
|
777
|
+
|
|
778
|
+
`;
|
|
779
|
+
if (features.includes('billing')) {
|
|
780
|
+
envContent += `# Stripe
|
|
781
|
+
STRIPE_PUBLISHABLE_KEY=
|
|
782
|
+
STRIPE_SECRET_KEY=
|
|
783
|
+
STRIPE_WEBHOOK_SECRET=
|
|
784
|
+
|
|
785
|
+
`;
|
|
786
|
+
}
|
|
787
|
+
// Widget / External Integration
|
|
788
|
+
envContent += `# CORS / Widget Configuration (External Chat Embedding)
|
|
789
|
+
# Comma-separated list of allowed origins for cross-origin requests (e.g. Blazor, Vue, React embeds)
|
|
790
|
+
# Use * to allow all origins (development only!)
|
|
791
|
+
# Example: CORS_ALLOWED_ORIGINS=https://my-blog.com,https://my-shop.com
|
|
792
|
+
CORS_ALLOWED_ORIGINS=
|
|
793
|
+
# Legacy alias (still supported): WIDGET_ALLOWED_ORIGINS
|
|
794
|
+
|
|
795
|
+
# Upstash Redis (optional — for rate-limiting in serverless/multi-instance)
|
|
796
|
+
# Without this, rate-limiting uses in-memory fallback (fine for single-instance)
|
|
797
|
+
# UPSTASH_REDIS_REST_URL=
|
|
798
|
+
# UPSTASH_REDIS_REST_TOKEN=
|
|
799
|
+
|
|
800
|
+
`;
|
|
801
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, '.env'), envContent);
|
|
802
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, '.env.example'), envContent
|
|
803
|
+
.replace(encryptionKey, 'your-64-char-hex-key')
|
|
804
|
+
.replace(nextAuthSecret, 'your-secret-key-change-in-production')
|
|
805
|
+
.replace(internalToken, 'chimerai_internal_your-token'));
|
|
806
|
+
}
|
|
807
|
+
async function copyFeatureFiles(targetDir, features) {
|
|
808
|
+
if (features.includes('auth')) {
|
|
809
|
+
// Create auth routes and pages using template generators
|
|
810
|
+
const nextAuthRoute = templates.generateNextAuthRoute();
|
|
811
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/auth/[...nextauth]'));
|
|
812
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/auth/[...nextauth]/route.ts'), nextAuthRoute);
|
|
813
|
+
const loginPage = templates.generateLoginPage();
|
|
814
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/auth/signin'));
|
|
815
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/auth/signin/page.tsx'), loginPage);
|
|
816
|
+
const sessionProvider = templates.generateSessionProvider();
|
|
817
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'components'));
|
|
818
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'components/SessionProvider.tsx'), sessionProvider);
|
|
819
|
+
const authLib = templates.generateAuthLib();
|
|
820
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'lib'));
|
|
821
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'lib/auth.ts'), authLib);
|
|
822
|
+
const nextAuthTypes = templates.generateNextAuthTypes();
|
|
823
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'types'));
|
|
824
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'types/next-auth.d.ts'), nextAuthTypes);
|
|
825
|
+
// Dashboard page + layout (authenticated landing page after login)
|
|
826
|
+
const dashboardPage = templates.generateDashboardPage(features);
|
|
827
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/dashboard'));
|
|
828
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/dashboard/page.tsx'), dashboardPage);
|
|
829
|
+
const dashboardLayout = templates.generateDashboardLayout();
|
|
830
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/dashboard/layout.tsx'), dashboardLayout);
|
|
831
|
+
// Dashboard profile page
|
|
832
|
+
const profilePage = templates.generateProfilePage();
|
|
833
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/dashboard/profile'));
|
|
834
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/dashboard/profile/page.tsx'), profilePage);
|
|
835
|
+
// Dashboard settings page
|
|
836
|
+
const settingsPage = templates.generateSettingsPage();
|
|
837
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/dashboard/settings'));
|
|
838
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/dashboard/settings/page.tsx'), settingsPage);
|
|
839
|
+
// User profile API route
|
|
840
|
+
const userProfileRoute = templates.generateUserProfileRoute();
|
|
841
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/user/profile'));
|
|
842
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/user/profile/route.ts'), userProfileRoute);
|
|
843
|
+
// GDPR self-service routes
|
|
844
|
+
const gdprExportRoute = templates.generateGdprDataExportRoute();
|
|
845
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/user/data-export'));
|
|
846
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/user/data-export/route.ts'), gdprExportRoute);
|
|
847
|
+
const gdprDeleteRoute = templates.generateGdprAccountDeleteRoute();
|
|
848
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/user/account'));
|
|
849
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/user/account/route.ts'), gdprDeleteRoute);
|
|
850
|
+
}
|
|
851
|
+
// Health check endpoint (used by Docker healthcheck and monitoring)
|
|
852
|
+
const healthRoute = templates.generateHealthRoute();
|
|
853
|
+
// Next.js middleware (security headers + CORS for widget endpoints)
|
|
854
|
+
const middleware = templates.generateMiddleware();
|
|
855
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'middleware.ts'), middleware);
|
|
856
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/health'));
|
|
857
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/health/route.ts'), healthRoute);
|
|
858
|
+
// Create Prisma utilities using template generators
|
|
859
|
+
const prismaLib = templates.generatePrismaLib();
|
|
860
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'lib'));
|
|
861
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'lib/prisma.ts'), prismaLib);
|
|
862
|
+
// ── Provider Infrastructure (ALWAYS generated — core) ─────────────
|
|
863
|
+
// Encryption library (AES-256-GCM)
|
|
864
|
+
const encryptionLib = templates.generateEncryptionLib();
|
|
865
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'lib/encryption.ts'), encryptionLib);
|
|
866
|
+
// API key auth helper
|
|
867
|
+
const apiKeyAuthLib = templates.generateApiKeyAuthLib();
|
|
868
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'lib/api-key-auth.ts'), apiKeyAuthLib);
|
|
869
|
+
// API protection middleware (auth checks, error responses, usage tracking)
|
|
870
|
+
// Only generated when auth is enabled (imports @/lib/auth)
|
|
871
|
+
if (features.includes('auth')) {
|
|
872
|
+
const apiProtectionLib = templates.generateApiProtectionLib();
|
|
873
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'lib/api-protection.ts'), apiProtectionLib);
|
|
874
|
+
// Dual-auth resolve helper (session + API key)
|
|
875
|
+
const resolveAuth = templates.generateResolveAuth();
|
|
876
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'lib/auth'));
|
|
877
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'lib/auth/resolve-auth.ts'), resolveAuth);
|
|
878
|
+
}
|
|
879
|
+
// ── Widget Infrastructure (embeddable chat for external apps) ──────
|
|
880
|
+
// Rate limiter (supports both session and API-key tiers)
|
|
881
|
+
const rateLimiter = templates.generateRateLimiter();
|
|
882
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'lib/rate-limit.ts'), rateLimiter);
|
|
883
|
+
// Widget bundle (Web Component + Shadow DOM, self-contained JS)
|
|
884
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'public/widget'));
|
|
885
|
+
const widgetBundle = templates.generateWidgetBundle();
|
|
886
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'public/widget/chat.js'), widgetBundle);
|
|
887
|
+
const widgetLoader = templates.generateWidgetLoader();
|
|
888
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'public/widget/loader.js'), widgetLoader);
|
|
889
|
+
// API-Key management routes
|
|
890
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/v1/api-keys'));
|
|
891
|
+
const apiKeysRoute = templates.generateApiKeysRoute();
|
|
892
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/v1/api-keys/route.ts'), apiKeysRoute);
|
|
893
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/v1/api-keys/[id]'));
|
|
894
|
+
const apiKeyIdRoute = templates.generateApiKeyIdRoute();
|
|
895
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/v1/api-keys/[id]/route.ts'), apiKeyIdRoute);
|
|
896
|
+
// API-Key management page (settings UI)
|
|
897
|
+
if (features.includes('auth')) {
|
|
898
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/(app)/settings/api-keys'));
|
|
899
|
+
const apiKeyPage = templates.generateApiKeyManagementPage();
|
|
900
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/(app)/settings/api-keys/page.tsx'), apiKeyPage);
|
|
901
|
+
}
|
|
902
|
+
// Notify provider change utility
|
|
903
|
+
const notifyLib = templates.generateNotifyProviderChangeLib();
|
|
904
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'lib/notify-provider-change.ts'), notifyLib);
|
|
905
|
+
// Provider CRUD route — /api/providers
|
|
906
|
+
const providerCrudRoute = templates.generateProviderCrudRoute();
|
|
907
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/providers'));
|
|
908
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/providers/route.ts'), providerCrudRoute);
|
|
909
|
+
// Provider [id] route — /api/providers/[id]
|
|
910
|
+
const providerIdRoute = templates.generateProviderIdRoute();
|
|
911
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/providers/[id]'));
|
|
912
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/providers/[id]/route.ts'), providerIdRoute);
|
|
913
|
+
// Provider test route — /api/providers/[id]/test
|
|
914
|
+
const providerTestRoute = templates.generateProviderTestRoute();
|
|
915
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/providers/[id]/test'));
|
|
916
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/providers/[id]/test/route.ts'), providerTestRoute);
|
|
917
|
+
// Provider sync route — /api/providers/[id]/sync
|
|
918
|
+
const providerSyncRoute = templates.generateProviderSyncRoute();
|
|
919
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/providers/[id]/sync'));
|
|
920
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/providers/[id]/sync/route.ts'), providerSyncRoute);
|
|
921
|
+
// Internal providers route — /api/internal/providers
|
|
922
|
+
const internalProvidersRoute = templates.generateInternalProvidersRoute();
|
|
923
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/internal/providers'));
|
|
924
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/internal/providers/route.ts'), internalProvidersRoute);
|
|
925
|
+
// Internal provider [id] route — /api/internal/providers/[id]
|
|
926
|
+
const internalProviderIdRoute = templates.generateInternalProviderIdRoute();
|
|
927
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/internal/providers/[id]'));
|
|
928
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/internal/providers/[id]/route.ts'), internalProviderIdRoute);
|
|
929
|
+
// Internal usage route — /api/internal/providers/[id]/usage
|
|
930
|
+
const internalUsageRoute = templates.generateInternalProviderUsageRoute();
|
|
931
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/internal/providers/[id]/usage'));
|
|
932
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/internal/providers/[id]/usage/route.ts'), internalUsageRoute);
|
|
933
|
+
// Provider Management page (always generated)
|
|
934
|
+
const providersPage = templates.generateModelProvidersPage();
|
|
935
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/dashboard/providers'));
|
|
936
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/dashboard/providers/page.tsx'), providersPage);
|
|
937
|
+
if (features.includes('admin')) {
|
|
938
|
+
// Admin layout with session check + admin role guard
|
|
939
|
+
const adminLayout = templates.generateAdminLayout();
|
|
940
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/admin'));
|
|
941
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/admin/layout.tsx'), adminLayout);
|
|
942
|
+
const adminDashboard = templates.generateAdminDashboardPage();
|
|
943
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/admin/page.tsx'), adminDashboard);
|
|
944
|
+
const adminUsers = templates.generateAdminUsersPage();
|
|
945
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/admin/users'));
|
|
946
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/admin/users/page.tsx'), adminUsers);
|
|
947
|
+
const adminRoles = templates.generateAdminRolesPage();
|
|
948
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/admin/roles'));
|
|
949
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/admin/roles/page.tsx'), adminRoles);
|
|
950
|
+
// Admin settings page
|
|
951
|
+
const adminSettings = templates.generateAdminSettingsPage();
|
|
952
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/admin/settings'));
|
|
953
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/admin/settings/page.tsx'), adminSettings);
|
|
954
|
+
// Audit log helper utility
|
|
955
|
+
const auditLogHelper = templates.generateAuditLogHelper();
|
|
956
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'lib'));
|
|
957
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'lib/audit.ts'), auditLogHelper);
|
|
958
|
+
// Admin audit logs page
|
|
959
|
+
const adminLogs = templates.generateAdminLogsPage();
|
|
960
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/admin/logs'));
|
|
961
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/admin/logs/page.tsx'), adminLogs);
|
|
962
|
+
// Audit logs API route
|
|
963
|
+
const auditLogsRoute = templates.generateAuditLogRoute();
|
|
964
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/admin/audit-logs'));
|
|
965
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/admin/audit-logs/route.ts'), auditLogsRoute);
|
|
966
|
+
// Create admin API routes
|
|
967
|
+
const adminUsersRoute = templates.generateAdminUsersRoute();
|
|
968
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/admin/users'));
|
|
969
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/admin/users/route.ts'), adminUsersRoute);
|
|
970
|
+
const adminUsersIdRoute = templates.generateAdminUsersIdRoute();
|
|
971
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/admin/users/[id]'));
|
|
972
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/admin/users/[id]/route.ts'), adminUsersIdRoute);
|
|
973
|
+
const adminRolesRoute = templates.generateAdminRolesRoute();
|
|
974
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/admin/roles'));
|
|
975
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/admin/roles/route.ts'), adminRolesRoute);
|
|
976
|
+
const adminRolesIdRoute = templates.generateAdminRolesIdRoute();
|
|
977
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/admin/roles/[id]'));
|
|
978
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/admin/roles/[id]/route.ts'), adminRolesIdRoute);
|
|
979
|
+
// Admin settings API route (GET, PUT)
|
|
980
|
+
const adminSettingsRoute = templates.generateAdminSettingsRoute();
|
|
981
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/admin/settings'));
|
|
982
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/admin/settings/route.ts'), adminSettingsRoute);
|
|
983
|
+
// Public app-settings API route (used by useAppName hook)
|
|
984
|
+
const appSettingsRoute = templates.generateAppSettingsRoute();
|
|
985
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/app-settings'));
|
|
986
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/app-settings/route.ts'), appSettingsRoute);
|
|
987
|
+
// useAppName client hook
|
|
988
|
+
const useAppNameHook = templates.generateUseAppNameHook();
|
|
989
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'lib'));
|
|
990
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'lib/use-app-name.ts'), useAppNameHook);
|
|
991
|
+
// ── RBAC Permission utilities (required by admin API routes) ───
|
|
992
|
+
const permissionsLib = `/**
|
|
993
|
+
* Permission utility functions
|
|
994
|
+
* Handles permission checks and role-based access control
|
|
995
|
+
*/
|
|
996
|
+
|
|
997
|
+
export const AVAILABLE_PERMISSIONS = [
|
|
998
|
+
'users:read',
|
|
999
|
+
'users:write',
|
|
1000
|
+
'users:delete',
|
|
1001
|
+
'roles:read',
|
|
1002
|
+
'roles:write',
|
|
1003
|
+
'roles:delete',
|
|
1004
|
+
'settings:read',
|
|
1005
|
+
'settings:write',
|
|
1006
|
+
'admin:*',
|
|
1007
|
+
] as const;
|
|
1008
|
+
|
|
1009
|
+
export type Permission = typeof AVAILABLE_PERMISSIONS[number];
|
|
1010
|
+
|
|
1011
|
+
interface User {
|
|
1012
|
+
id: string;
|
|
1013
|
+
email: string;
|
|
1014
|
+
roles?: Array<{ permissions: string[] }>;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
export function hasPermission(user: User | null, permission: string): boolean {
|
|
1018
|
+
if (!user || !user.roles) return false;
|
|
1019
|
+
const allPermissions = user.roles.flatMap(role => role.permissions || []);
|
|
1020
|
+
|
|
1021
|
+
// Tier 1: Super-Wildcard — '*' matcht ALLES
|
|
1022
|
+
if (allPermissions.includes('*')) return true;
|
|
1023
|
+
|
|
1024
|
+
// Tier 2: Kategorie-Wildcard — 'admin:*' matcht 'admin:users:read', 'admin:roles:write', etc.
|
|
1025
|
+
for (const perm of allPermissions) {
|
|
1026
|
+
if (perm.endsWith(':*')) {
|
|
1027
|
+
const prefix = perm.slice(0, -1); // 'admin:*' → 'admin:'
|
|
1028
|
+
if (permission.startsWith(prefix)) return true;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// Tier 3: Exakter Match
|
|
1033
|
+
return allPermissions.includes(permission);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
export function hasAnyPermission(user: User | null, permissions: string[]): boolean {
|
|
1037
|
+
if (!user) return false;
|
|
1038
|
+
return permissions.some(permission => hasPermission(user, permission));
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
export function hasAllPermissions(user: User | null, permissions: string[]): boolean {
|
|
1042
|
+
if (!user) return false;
|
|
1043
|
+
return permissions.every(permission => hasPermission(user, permission));
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
export function getUserPermissions(user: User | null): string[] {
|
|
1047
|
+
if (!user || !user.roles) return [];
|
|
1048
|
+
return [...new Set(user.roles.flatMap(role => role.permissions || []))];
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
export function isValidPermission(permission: string): boolean {
|
|
1052
|
+
return AVAILABLE_PERMISSIONS.includes(permission as Permission);
|
|
1053
|
+
}
|
|
1054
|
+
`;
|
|
1055
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'lib/permissions.ts'), permissionsLib);
|
|
1056
|
+
const requirePermissionLib = `import { getServerSession } from 'next-auth';
|
|
1057
|
+
import { NextResponse } from 'next/server';
|
|
1058
|
+
import { authOptions } from '@/lib/auth';
|
|
1059
|
+
import { hasPermission } from '@/lib/permissions';
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Server-side permission check for API routes
|
|
1063
|
+
* Returns NextResponse with 401/403 if check fails, null if OK
|
|
1064
|
+
*/
|
|
1065
|
+
export async function requirePermission(permission: string) {
|
|
1066
|
+
const session = await getServerSession(authOptions);
|
|
1067
|
+
|
|
1068
|
+
if (!session || !session.user) {
|
|
1069
|
+
return NextResponse.json(
|
|
1070
|
+
{ error: 'Unauthorized - Please sign in' },
|
|
1071
|
+
{ status: 401 }
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const user = await getServerSessionWithPermissions();
|
|
1076
|
+
|
|
1077
|
+
if (!user || !hasPermission(user as any, permission)) {
|
|
1078
|
+
return NextResponse.json(
|
|
1079
|
+
{ error: \`Forbidden - Required permission: \${permission}\` },
|
|
1080
|
+
{ status: 403 }
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
return null;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
async function getServerSessionWithPermissions() {
|
|
1088
|
+
const session = await getServerSession(authOptions);
|
|
1089
|
+
if (!session?.user?.email) return null;
|
|
1090
|
+
|
|
1091
|
+
const { prisma } = await import('@/lib/prisma');
|
|
1092
|
+
const user = await prisma.user.findUnique({
|
|
1093
|
+
where: { email: session.user.email },
|
|
1094
|
+
include: {
|
|
1095
|
+
roles: {
|
|
1096
|
+
select: {
|
|
1097
|
+
role: {
|
|
1098
|
+
select: {
|
|
1099
|
+
id: true,
|
|
1100
|
+
name: true,
|
|
1101
|
+
permissions: true
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
if (!user) return null;
|
|
1110
|
+
|
|
1111
|
+
// Flatten UserRole[] → { permissions: string[] }[]
|
|
1112
|
+
return {
|
|
1113
|
+
...user,
|
|
1114
|
+
roles: (user.roles as any[]).map((ur: any) => ur.role),
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
`;
|
|
1118
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'lib/auth'));
|
|
1119
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'lib/auth/require-permission.ts'), requirePermissionLib);
|
|
1120
|
+
}
|
|
1121
|
+
if (features.includes('prompts')) {
|
|
1122
|
+
const promptsPage = templates.generatePromptManagementPage();
|
|
1123
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/dashboard/prompts'));
|
|
1124
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/dashboard/prompts/page.tsx'), promptsPage);
|
|
1125
|
+
// Prompts API routes
|
|
1126
|
+
const promptsRoute = templates.generatePromptsRoute();
|
|
1127
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/prompts'));
|
|
1128
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/prompts/route.ts'), promptsRoute);
|
|
1129
|
+
const promptsIdRoute = templates.generatePromptsIdRoute();
|
|
1130
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/prompts/[id]'));
|
|
1131
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/prompts/[id]/route.ts'), promptsIdRoute);
|
|
1132
|
+
}
|
|
1133
|
+
if (features.includes('chat')) {
|
|
1134
|
+
// Modular chat components (useChat hook + individual components)
|
|
1135
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'components/chat'));
|
|
1136
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'components/chat/use-chat.ts'), templates.generateUseChatHook());
|
|
1137
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'components/chat/chat-message.tsx'), templates.generateChatMessage());
|
|
1138
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'components/chat/chat-input.tsx'), templates.generateChatInput());
|
|
1139
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'components/chat/chat-sidebar.tsx'), templates.generateChatSidebar());
|
|
1140
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'components/chat/model-selector.tsx'), templates.generateModelSelector());
|
|
1141
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'components/chat/index.ts'), [
|
|
1142
|
+
"export { useChat } from './use-chat';",
|
|
1143
|
+
"export { ChatMessage } from './chat-message';",
|
|
1144
|
+
"export { ChatInput } from './chat-input';",
|
|
1145
|
+
"export { ChatSidebar } from './chat-sidebar';",
|
|
1146
|
+
"export { ModelSelector } from './model-selector';",
|
|
1147
|
+
"export type { ChatMessageData, MessageActions, ConversationItem, ModelOption } from './use-chat';",
|
|
1148
|
+
].join('\n') + '\n');
|
|
1149
|
+
// Chat page
|
|
1150
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/(app)/chat'));
|
|
1151
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/(app)/chat/page.tsx'), templates.generateChatPage());
|
|
1152
|
+
// API routes
|
|
1153
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/v1/chat/stream'));
|
|
1154
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/v1/chat/stream/route.ts'), templates.generateChatStreamRouteWithPersistence());
|
|
1155
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/conversations'));
|
|
1156
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/conversations/route.ts'), templates.generateConversationsRoute());
|
|
1157
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/conversations/[id]'));
|
|
1158
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/conversations/[id]/route.ts'), templates.generateConversationDetailRoute());
|
|
1159
|
+
// Models listing route (used by ModelSelector in chat — session-only, internal)
|
|
1160
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/models'));
|
|
1161
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/models/route.ts'), templates.generateModelsRoute());
|
|
1162
|
+
// Public v1 models route (dual-auth: session OR API-key, for widgets/external apps)
|
|
1163
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/v1/models'));
|
|
1164
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/v1/models/route.ts'), templates.generateV1ModelsRoute());
|
|
1165
|
+
}
|
|
1166
|
+
// ── Billing / Stripe (when selected) ────────────────────────────
|
|
1167
|
+
if (features.includes('billing')) {
|
|
1168
|
+
const stripeLib = templates.generateStripeLib();
|
|
1169
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'lib'));
|
|
1170
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'lib/stripe.ts'), stripeLib);
|
|
1171
|
+
const billingPage = templates.generateBillingPage();
|
|
1172
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/billing'));
|
|
1173
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/billing/page.tsx'), billingPage);
|
|
1174
|
+
const checkoutRoute = templates.generateCheckoutRoute();
|
|
1175
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/billing/checkout'));
|
|
1176
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/billing/checkout/route.ts'), checkoutRoute);
|
|
1177
|
+
const portalRoute = templates.generatePortalRoute();
|
|
1178
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/billing/portal'));
|
|
1179
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/billing/portal/route.ts'), portalRoute);
|
|
1180
|
+
const subscriptionRoute = templates.generateSubscriptionRoute();
|
|
1181
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/billing/subscription'));
|
|
1182
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/billing/subscription/route.ts'), subscriptionRoute);
|
|
1183
|
+
const stripeWebhook = templates.generateStripeWebhookRoute();
|
|
1184
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/webhooks/stripe'));
|
|
1185
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/webhooks/stripe/route.ts'), stripeWebhook);
|
|
1186
|
+
}
|
|
1187
|
+
// ── RAG / Vector Store (when selected) ──────────────────────────
|
|
1188
|
+
if (features.includes('rag')) {
|
|
1189
|
+
const ragLib = templates.generateRagLib();
|
|
1190
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'lib'));
|
|
1191
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'lib/rag.ts'), ragLib);
|
|
1192
|
+
const ragPage = templates.generateRagPage();
|
|
1193
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/rag'));
|
|
1194
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/rag/page.tsx'), ragPage);
|
|
1195
|
+
const ragUploadRoute = templates.generateRagUploadRoute();
|
|
1196
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/rag'));
|
|
1197
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/rag/route.ts'), ragUploadRoute);
|
|
1198
|
+
const ragQueryRoute = templates.generateRagQueryRoute();
|
|
1199
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/rag/query'));
|
|
1200
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/rag/query/route.ts'), ragQueryRoute);
|
|
1201
|
+
const ragStatsRoute = templates.generateRagStatsRoute();
|
|
1202
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/rag/stats'));
|
|
1203
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/rag/stats/route.ts'), ragStatsRoute);
|
|
1204
|
+
const ragClearRoute = templates.generateRagClearRoute();
|
|
1205
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app/api/rag/clear'));
|
|
1206
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app/api/rag/clear/route.ts'), ragClearRoute);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
async function createSeedScript(targetDir, features, sqlite) {
|
|
1210
|
+
// Generate seed script with feature-specific configuration
|
|
1211
|
+
const seedScript = `import { PrismaClient } from '@prisma/client';
|
|
1212
|
+
import * as bcrypt from 'bcryptjs';
|
|
1213
|
+
import crypto from 'crypto';
|
|
1214
|
+
|
|
1215
|
+
const prisma = new PrismaClient();
|
|
1216
|
+
|
|
1217
|
+
// ── Encryption helpers (same as lib/encryption.ts) ──────────────
|
|
1218
|
+
function getKey(): Buffer {
|
|
1219
|
+
const key = process.env.PROVIDER_ENCRYPTION_KEY;
|
|
1220
|
+
if (!key) throw new Error('PROVIDER_ENCRYPTION_KEY required for seeding');
|
|
1221
|
+
if (key.length === 64 && /^[0-9a-fA-F]+$/.test(key)) {
|
|
1222
|
+
return Buffer.from(key, 'hex');
|
|
1223
|
+
}
|
|
1224
|
+
return crypto.createHash('sha256').update(key).digest();
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function encrypt(text: string): string {
|
|
1228
|
+
if (!text) return '';
|
|
1229
|
+
const key = getKey();
|
|
1230
|
+
const iv = crypto.randomBytes(16);
|
|
1231
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv, { authTagLength: 16 });
|
|
1232
|
+
let encrypted = cipher.update(text, 'utf8', 'base64');
|
|
1233
|
+
encrypted += cipher.final('base64');
|
|
1234
|
+
const authTag = cipher.getAuthTag();
|
|
1235
|
+
return iv.toString('base64') + ':' + authTag.toString('base64') + ':' + encrypted;
|
|
1236
|
+
}
|
|
1237
|
+
// ────────────────────────────────────────────────────────────────
|
|
1238
|
+
|
|
1239
|
+
async function main() {
|
|
1240
|
+
console.log('🌱 Seeding database...');
|
|
1241
|
+
|
|
1242
|
+
// Create default admin user
|
|
1243
|
+
const hashedPassword = await bcrypt.hash('admin123', 10);
|
|
1244
|
+
const admin = await prisma.user.upsert({
|
|
1245
|
+
where: { email: 'admin@example.com' },
|
|
1246
|
+
update: {},
|
|
1247
|
+
create: {
|
|
1248
|
+
email: 'admin@example.com',
|
|
1249
|
+
name: 'Admin User',
|
|
1250
|
+
password: hashedPassword,
|
|
1251
|
+
},
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
console.log('✅ Admin user created: admin@example.com / admin123');
|
|
1255
|
+
${features.includes('rbac')
|
|
1256
|
+
? `
|
|
1257
|
+
${sqlite
|
|
1258
|
+
? ` // Create default roles (SQLite-compatible: individual upserts)
|
|
1259
|
+
for (const role of [
|
|
1260
|
+
{ name: 'admin', description: 'Full system access', permissions: JSON.stringify(['*']) },
|
|
1261
|
+
{ name: 'user', description: 'Regular user access', permissions: JSON.stringify(['chat:read', 'chat:write', 'profile:read']) },
|
|
1262
|
+
{ name: 'viewer', description: 'Read-only access', permissions: JSON.stringify(['chat:read']) },
|
|
1263
|
+
]) {
|
|
1264
|
+
await prisma.role.upsert({
|
|
1265
|
+
where: { name: role.name },
|
|
1266
|
+
update: {},
|
|
1267
|
+
create: role,
|
|
1268
|
+
});
|
|
1269
|
+
}`
|
|
1270
|
+
: ` // Create default roles
|
|
1271
|
+
await prisma.role.createMany({
|
|
1272
|
+
data: [
|
|
1273
|
+
{
|
|
1274
|
+
name: 'admin',
|
|
1275
|
+
description: 'Full system access',
|
|
1276
|
+
permissions: ['*'],
|
|
1277
|
+
},
|
|
1278
|
+
{
|
|
1279
|
+
name: 'user',
|
|
1280
|
+
description: 'Regular user access',
|
|
1281
|
+
permissions: ['chat:read', 'chat:write', 'profile:read'],
|
|
1282
|
+
},
|
|
1283
|
+
{
|
|
1284
|
+
name: 'viewer',
|
|
1285
|
+
description: 'Read-only access',
|
|
1286
|
+
permissions: ['chat:read'],
|
|
1287
|
+
},
|
|
1288
|
+
],
|
|
1289
|
+
skipDuplicates: true,
|
|
1290
|
+
});`}
|
|
1291
|
+
|
|
1292
|
+
console.log('✅ Default roles created');
|
|
1293
|
+
|
|
1294
|
+
// Assign admin role to admin user
|
|
1295
|
+
const adminRole = await prisma.role.findUnique({ where: { name: 'admin' } });
|
|
1296
|
+
if (adminRole) {
|
|
1297
|
+
await prisma.userRole.upsert({
|
|
1298
|
+
where: { userId_roleId: { userId: admin.id, roleId: adminRole.id } },
|
|
1299
|
+
update: {},
|
|
1300
|
+
create: { userId: admin.id, roleId: adminRole.id },
|
|
1301
|
+
});
|
|
1302
|
+
console.log('✅ Admin user assigned admin role');
|
|
1303
|
+
}
|
|
1304
|
+
`
|
|
1305
|
+
: ''}
|
|
1306
|
+
// ── Seed Providers (if API keys are in .env) ────────────────────
|
|
1307
|
+
const openaiKey = process.env.OPENAI_API_KEY;
|
|
1308
|
+
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
1309
|
+
|
|
1310
|
+
if (openaiKey) {
|
|
1311
|
+
const provider = await prisma.provider.upsert({
|
|
1312
|
+
where: { id: 'seed-openai' },
|
|
1313
|
+
update: {},
|
|
1314
|
+
create: {
|
|
1315
|
+
id: 'seed-openai',
|
|
1316
|
+
name: 'OpenAI',
|
|
1317
|
+
type: 'openai',
|
|
1318
|
+
description: 'OpenAI API (seeded from .env)',
|
|
1319
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
1320
|
+
apiKey: encrypt(openaiKey),
|
|
1321
|
+
config: ${sqlite ? `JSON.stringify({ defaultModel: 'gpt-4o-mini' })` : `{ defaultModel: 'gpt-4o-mini' }`},
|
|
1322
|
+
status: 'active',
|
|
1323
|
+
isDefault: true,
|
|
1324
|
+
priority: 0,
|
|
1325
|
+
createdBy: admin.id,
|
|
1326
|
+
},
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
// Create default OpenAI models
|
|
1330
|
+
${sqlite
|
|
1331
|
+
? ` for (const m of [
|
|
1332
|
+
{ providerId: provider.id, modelId: 'gpt-4o', name: 'GPT-4o', capabilities: JSON.stringify(['chat', 'vision']), contextWindow: 128000, inputCost: 2.5, outputCost: 10 },
|
|
1333
|
+
{ providerId: provider.id, modelId: 'gpt-4o-mini', name: 'GPT-4o Mini', capabilities: JSON.stringify(['chat']), contextWindow: 128000, inputCost: 0.15, outputCost: 0.6 },
|
|
1334
|
+
{ providerId: provider.id, modelId: 'gpt-4-turbo', name: 'GPT-4 Turbo', capabilities: JSON.stringify(['chat', 'vision']), contextWindow: 128000, inputCost: 10, outputCost: 30 },
|
|
1335
|
+
{ providerId: provider.id, modelId: 'text-embedding-3-small', name: 'Embedding 3 Small', capabilities: JSON.stringify(['embedding']), contextWindow: 8191, inputCost: 0.02, outputCost: 0 },
|
|
1336
|
+
{ providerId: provider.id, modelId: 'text-embedding-3-large', name: 'Embedding 3 Large', capabilities: JSON.stringify(['embedding']), contextWindow: 8191, inputCost: 0.13, outputCost: 0 },
|
|
1337
|
+
]) {
|
|
1338
|
+
try { await prisma.model.create({ data: m }); } catch { /* skip duplicate */ }
|
|
1339
|
+
}`
|
|
1340
|
+
: ` await prisma.model.createMany({
|
|
1341
|
+
data: [
|
|
1342
|
+
{ providerId: provider.id, modelId: 'gpt-4o', name: 'GPT-4o', capabilities: ['chat', 'vision'], contextWindow: 128000, inputCost: 2.5, outputCost: 10 },
|
|
1343
|
+
{ providerId: provider.id, modelId: 'gpt-4o-mini', name: 'GPT-4o Mini', capabilities: ['chat'], contextWindow: 128000, inputCost: 0.15, outputCost: 0.6 },
|
|
1344
|
+
{ providerId: provider.id, modelId: 'gpt-4-turbo', name: 'GPT-4 Turbo', capabilities: ['chat', 'vision'], contextWindow: 128000, inputCost: 10, outputCost: 30 },
|
|
1345
|
+
{ providerId: provider.id, modelId: 'text-embedding-3-small', name: 'Embedding 3 Small', capabilities: ['embedding'], contextWindow: 8191, inputCost: 0.02, outputCost: 0 },
|
|
1346
|
+
{ providerId: provider.id, modelId: 'text-embedding-3-large', name: 'Embedding 3 Large', capabilities: ['embedding'], contextWindow: 8191, inputCost: 0.13, outputCost: 0 },
|
|
1347
|
+
],
|
|
1348
|
+
skipDuplicates: true,
|
|
1349
|
+
});`}
|
|
1350
|
+
|
|
1351
|
+
console.log('✅ OpenAI provider seeded with models');
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
if (anthropicKey) {
|
|
1355
|
+
const provider = await prisma.provider.upsert({
|
|
1356
|
+
where: { id: 'seed-anthropic' },
|
|
1357
|
+
update: {},
|
|
1358
|
+
create: {
|
|
1359
|
+
id: 'seed-anthropic',
|
|
1360
|
+
name: 'Anthropic',
|
|
1361
|
+
type: 'anthropic',
|
|
1362
|
+
description: 'Anthropic Claude API (seeded from .env)',
|
|
1363
|
+
baseUrl: 'https://api.anthropic.com/v1',
|
|
1364
|
+
apiKey: encrypt(anthropicKey),
|
|
1365
|
+
config: ${sqlite ? `JSON.stringify({ defaultModel: 'claude-sonnet-4-20250514' })` : `{ defaultModel: 'claude-sonnet-4-20250514' }`},
|
|
1366
|
+
status: 'active',
|
|
1367
|
+
isDefault: false,
|
|
1368
|
+
priority: 1,
|
|
1369
|
+
createdBy: admin.id,
|
|
1370
|
+
},
|
|
1371
|
+
});
|
|
1372
|
+
|
|
1373
|
+
${sqlite
|
|
1374
|
+
? ` for (const m of [
|
|
1375
|
+
{ providerId: provider.id, modelId: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4', capabilities: JSON.stringify(['chat', 'vision']), contextWindow: 200000, inputCost: 3, outputCost: 15 },
|
|
1376
|
+
{ providerId: provider.id, modelId: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet', capabilities: JSON.stringify(['chat', 'vision']), contextWindow: 200000, inputCost: 3, outputCost: 15 },
|
|
1377
|
+
{ providerId: provider.id, modelId: 'claude-3-haiku-20240307', name: 'Claude 3 Haiku', capabilities: JSON.stringify(['chat']), contextWindow: 200000, inputCost: 0.25, outputCost: 1.25 },
|
|
1378
|
+
]) {
|
|
1379
|
+
try { await prisma.model.create({ data: m }); } catch { /* skip duplicate */ }
|
|
1380
|
+
}`
|
|
1381
|
+
: ` await prisma.model.createMany({
|
|
1382
|
+
data: [
|
|
1383
|
+
{ providerId: provider.id, modelId: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4', capabilities: ['chat', 'vision'], contextWindow: 200000, inputCost: 3, outputCost: 15 },
|
|
1384
|
+
{ providerId: provider.id, modelId: 'claude-3-5-sonnet-20241022', name: 'Claude 3.5 Sonnet', capabilities: ['chat', 'vision'], contextWindow: 200000, inputCost: 3, outputCost: 15 },
|
|
1385
|
+
{ providerId: provider.id, modelId: 'claude-3-haiku-20240307', name: 'Claude 3 Haiku', capabilities: ['chat'], contextWindow: 200000, inputCost: 0.25, outputCost: 1.25 },
|
|
1386
|
+
],
|
|
1387
|
+
skipDuplicates: true,
|
|
1388
|
+
});`}
|
|
1389
|
+
|
|
1390
|
+
console.log('✅ Anthropic provider seeded with models');
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
if (!openaiKey && !anthropicKey) {
|
|
1394
|
+
console.log('ℹ️ No API keys in .env — skip provider seeding. Add via Provider Management UI.');
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
console.log('🎉 Seeding completed!');
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
main()
|
|
1401
|
+
.catch((e) => {
|
|
1402
|
+
console.error('❌ Seeding failed:', e);
|
|
1403
|
+
process.exit(1);
|
|
1404
|
+
})
|
|
1405
|
+
.finally(async () => {
|
|
1406
|
+
await prisma.$disconnect();
|
|
1407
|
+
});
|
|
1408
|
+
`;
|
|
1409
|
+
const seedDest = path_1.default.join(targetDir, 'prisma/seed.ts');
|
|
1410
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(seedDest));
|
|
1411
|
+
await fs_extra_1.default.writeFile(seedDest, seedScript);
|
|
1412
|
+
console.log(chalk_1.default.green(' ✓ Created seed script'));
|
|
1413
|
+
}
|
|
1414
|
+
async function createDockerCompose(targetDir) {
|
|
1415
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'docker-compose.yml'), (0, index_js_1.generateDockerComposeDev)());
|
|
1416
|
+
}
|
|
1417
|
+
async function createInstallScripts(targetDir) {
|
|
1418
|
+
// Windows install.bat
|
|
1419
|
+
const installBat = `@echo off
|
|
1420
|
+
REM Installation Script for ChimerAI Project
|
|
1421
|
+
echo.
|
|
1422
|
+
echo ================================================
|
|
1423
|
+
echo ChimerAI Project Setup
|
|
1424
|
+
echo ================================================
|
|
1425
|
+
echo.
|
|
1426
|
+
|
|
1427
|
+
REM Check Node.js
|
|
1428
|
+
where node >nul 2>nul
|
|
1429
|
+
if %ERRORLEVEL% NEQ 0 (
|
|
1430
|
+
echo [ERROR] Node.js is not installed
|
|
1431
|
+
pause
|
|
1432
|
+
exit /b 1
|
|
1433
|
+
)
|
|
1434
|
+
|
|
1435
|
+
REM Check Docker
|
|
1436
|
+
where docker >nul 2>nul
|
|
1437
|
+
if %ERRORLEVEL% NEQ 0 (
|
|
1438
|
+
echo [ERROR] Docker is not installed
|
|
1439
|
+
pause
|
|
1440
|
+
exit /b 1
|
|
1441
|
+
)
|
|
1442
|
+
|
|
1443
|
+
echo [1/5] Installing dependencies...
|
|
1444
|
+
call npm install
|
|
1445
|
+
if %ERRORLEVEL% NEQ 0 (
|
|
1446
|
+
echo [ERROR] Failed to install dependencies
|
|
1447
|
+
pause
|
|
1448
|
+
exit /b 1
|
|
1449
|
+
)
|
|
1450
|
+
|
|
1451
|
+
echo [2/5] Starting Docker containers...
|
|
1452
|
+
call docker-compose up -d
|
|
1453
|
+
if %ERRORLEVEL% NEQ 0 (
|
|
1454
|
+
echo [ERROR] Failed to start Docker
|
|
1455
|
+
pause
|
|
1456
|
+
exit /b 1
|
|
1457
|
+
)
|
|
1458
|
+
|
|
1459
|
+
echo [3/5] Waiting for database...
|
|
1460
|
+
timeout /t 5 /nobreak >nul
|
|
1461
|
+
|
|
1462
|
+
echo [4/5] Setting up database...
|
|
1463
|
+
call npm run db:push
|
|
1464
|
+
if %ERRORLEVEL% NEQ 0 (
|
|
1465
|
+
echo [ERROR] Failed to setup database
|
|
1466
|
+
pause
|
|
1467
|
+
exit /b 1
|
|
1468
|
+
)
|
|
1469
|
+
|
|
1470
|
+
echo [5/5] Seeding database...
|
|
1471
|
+
call npm run db:seed
|
|
1472
|
+
if %ERRORLEVEL% NEQ 0 (
|
|
1473
|
+
echo [WARNING] Seeding failed, but you can continue
|
|
1474
|
+
)
|
|
1475
|
+
|
|
1476
|
+
echo.
|
|
1477
|
+
echo ================================================
|
|
1478
|
+
echo Setup completed successfully!
|
|
1479
|
+
echo ================================================
|
|
1480
|
+
echo.
|
|
1481
|
+
echo Next steps:
|
|
1482
|
+
echo npm run dev
|
|
1483
|
+
echo Open: http://localhost:3001
|
|
1484
|
+
echo.
|
|
1485
|
+
echo Login with:
|
|
1486
|
+
echo Email: admin@example.com
|
|
1487
|
+
echo Password: admin123
|
|
1488
|
+
echo.
|
|
1489
|
+
pause
|
|
1490
|
+
`;
|
|
1491
|
+
// Linux/macOS install.sh
|
|
1492
|
+
const installSh = `#!/bin/bash
|
|
1493
|
+
set -e
|
|
1494
|
+
|
|
1495
|
+
echo ""
|
|
1496
|
+
echo "================================================"
|
|
1497
|
+
echo " ChimerAI Project Setup"
|
|
1498
|
+
echo "================================================"
|
|
1499
|
+
echo ""
|
|
1500
|
+
|
|
1501
|
+
# Check Node.js
|
|
1502
|
+
if ! command -v node &> /dev/null; then
|
|
1503
|
+
echo "[ERROR] Node.js is not installed"
|
|
1504
|
+
exit 1
|
|
1505
|
+
fi
|
|
1506
|
+
|
|
1507
|
+
# Check Docker
|
|
1508
|
+
if ! command -v docker &> /dev/null; then
|
|
1509
|
+
echo "[ERROR] Docker is not installed"
|
|
1510
|
+
exit 1
|
|
1511
|
+
fi
|
|
1512
|
+
|
|
1513
|
+
echo "[1/5] Installing dependencies..."
|
|
1514
|
+
npm install
|
|
1515
|
+
|
|
1516
|
+
echo "[2/5] Starting Docker containers..."
|
|
1517
|
+
docker-compose up -d
|
|
1518
|
+
|
|
1519
|
+
echo "[3/5] Waiting for database..."
|
|
1520
|
+
sleep 5
|
|
1521
|
+
|
|
1522
|
+
echo "[4/5] Setting up database..."
|
|
1523
|
+
npm run db:push
|
|
1524
|
+
|
|
1525
|
+
echo "[5/5] Seeding database..."
|
|
1526
|
+
npm run db:seed || echo "[WARNING] Seeding failed, but you can continue"
|
|
1527
|
+
|
|
1528
|
+
echo ""
|
|
1529
|
+
echo "================================================"
|
|
1530
|
+
echo " Setup completed successfully!"
|
|
1531
|
+
echo "================================================"
|
|
1532
|
+
echo ""
|
|
1533
|
+
echo "Next steps:"
|
|
1534
|
+
echo " npm run dev"
|
|
1535
|
+
echo " Open: http://localhost:3001"
|
|
1536
|
+
echo ""
|
|
1537
|
+
echo "Login with:"
|
|
1538
|
+
echo " Email: admin@example.com"
|
|
1539
|
+
echo " Password: admin123"
|
|
1540
|
+
echo ""
|
|
1541
|
+
`;
|
|
1542
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'install.bat'), installBat);
|
|
1543
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'install.sh'), installSh);
|
|
1544
|
+
// Make install.sh executable
|
|
1545
|
+
try {
|
|
1546
|
+
await fs_extra_1.default.chmod(path_1.default.join(targetDir, 'install.sh'), '755');
|
|
1547
|
+
}
|
|
1548
|
+
catch (error) {
|
|
1549
|
+
// Ignore on Windows
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
async function createReadme(targetDir, projectName, features) {
|
|
1553
|
+
let readme = `# ${projectName}
|
|
1554
|
+
|
|
1555
|
+
Built with ChimerAI Kickstart
|
|
1556
|
+
|
|
1557
|
+
## Features
|
|
1558
|
+
|
|
1559
|
+
`;
|
|
1560
|
+
const featureDescriptions = {
|
|
1561
|
+
auth: '- 🔐 Authentication with NextAuth',
|
|
1562
|
+
rbac: '- 👥 Role-Based Access Control (RBAC)',
|
|
1563
|
+
'model-providers': '- 🔌 AI Model Provider Management',
|
|
1564
|
+
prompts: '- 📝 Prompt Template System',
|
|
1565
|
+
chat: '- 💬 AI Chat Interface',
|
|
1566
|
+
rag: '- 🔍 RAG / Vector Store',
|
|
1567
|
+
billing: '- 💳 Stripe Billing Integration',
|
|
1568
|
+
analytics: '- 📊 API Usage Analytics',
|
|
1569
|
+
admin: '- 🎨 Admin Dashboard',
|
|
1570
|
+
};
|
|
1571
|
+
features.forEach((f) => {
|
|
1572
|
+
if (featureDescriptions[f]) {
|
|
1573
|
+
readme += featureDescriptions[f] + '\n';
|
|
1574
|
+
}
|
|
1575
|
+
});
|
|
1576
|
+
readme += `
|
|
1577
|
+
|
|
1578
|
+
## Getting Started
|
|
1579
|
+
|
|
1580
|
+
### Prerequisites
|
|
1581
|
+
|
|
1582
|
+
- Node.js 20+
|
|
1583
|
+
- Docker Desktop
|
|
1584
|
+
- npm
|
|
1585
|
+
|
|
1586
|
+
### Quick Start (Recommended)
|
|
1587
|
+
|
|
1588
|
+
**Windows:**
|
|
1589
|
+
\`\`\`bash
|
|
1590
|
+
install.bat
|
|
1591
|
+
\`\`\`
|
|
1592
|
+
|
|
1593
|
+
**Linux/macOS:**
|
|
1594
|
+
\`\`\`bash
|
|
1595
|
+
./install.sh
|
|
1596
|
+
\`\`\`
|
|
1597
|
+
|
|
1598
|
+
The install script will automatically:
|
|
1599
|
+
- Install dependencies
|
|
1600
|
+
- Start Docker containers
|
|
1601
|
+
- Setup and seed the database
|
|
1602
|
+
- Show you the next steps
|
|
1603
|
+
|
|
1604
|
+
### Manual Installation
|
|
1605
|
+
|
|
1606
|
+
If you prefer to run commands manually:
|
|
1607
|
+
|
|
1608
|
+
\`\`\`bash
|
|
1609
|
+
# Install dependencies
|
|
1610
|
+
npm install
|
|
1611
|
+
|
|
1612
|
+
# Start Docker services
|
|
1613
|
+
docker-compose up -d
|
|
1614
|
+
|
|
1615
|
+
# Setup database
|
|
1616
|
+
npm run db:push
|
|
1617
|
+
npm run db:seed
|
|
1618
|
+
|
|
1619
|
+
# Start development server
|
|
1620
|
+
npm run dev
|
|
1621
|
+
\`\`\`
|
|
1622
|
+
|
|
1623
|
+
Open [http://localhost:3001](http://localhost:3001) in your browser.
|
|
1624
|
+
|
|
1625
|
+
### Default Admin Credentials
|
|
1626
|
+
|
|
1627
|
+
- Email: admin@example.com
|
|
1628
|
+
- Password: admin123
|
|
1629
|
+
|
|
1630
|
+
⚠️ Change these in production!
|
|
1631
|
+
|
|
1632
|
+
## Available Scripts
|
|
1633
|
+
|
|
1634
|
+
- \`pnpm dev\` - Start development server
|
|
1635
|
+
- \`pnpm build\` - Build for production
|
|
1636
|
+
- \`pnpm start\` - Start production server
|
|
1637
|
+
- \`pnpm lint\` - Run linter
|
|
1638
|
+
- \`pnpm db:push\` - Push database schema
|
|
1639
|
+
- \`pnpm db:seed\` - Seed database
|
|
1640
|
+
- \`pnpm db:studio\` - Open Prisma Studio
|
|
1641
|
+
|
|
1642
|
+
## Tech Stack
|
|
1643
|
+
|
|
1644
|
+
- **Framework**: Next.js 15
|
|
1645
|
+
- **Language**: TypeScript
|
|
1646
|
+
- **Database**: PostgreSQL + Prisma
|
|
1647
|
+
- **Auth**: NextAuth.js
|
|
1648
|
+
- **Styling**: Tailwind CSS
|
|
1649
|
+
- **UI Components**: Radix UI
|
|
1650
|
+
|
|
1651
|
+
## Project Structure
|
|
1652
|
+
|
|
1653
|
+
\`\`\`
|
|
1654
|
+
├── app/ # Next.js app directory
|
|
1655
|
+
├── components/ # React components
|
|
1656
|
+
├── lib/ # Utility functions
|
|
1657
|
+
├── prisma/ # Database schema
|
|
1658
|
+
└── public/ # Static assets
|
|
1659
|
+
\`\`\`
|
|
1660
|
+
|
|
1661
|
+
## Learn More
|
|
1662
|
+
|
|
1663
|
+
- [ChimerAI Documentation](https://chimerai.dev)
|
|
1664
|
+
- [Next.js Documentation](https://nextjs.org/docs)
|
|
1665
|
+
- [Prisma Documentation](https://www.prisma.io/docs)
|
|
1666
|
+
|
|
1667
|
+
## License
|
|
1668
|
+
|
|
1669
|
+
Commercial License - See LICENSE file
|
|
1670
|
+
`;
|
|
1671
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'README.md'), readme);
|
|
1672
|
+
// Create docs directory with inline documentation
|
|
1673
|
+
// No external path references — CLI works standalone as npm package
|
|
1674
|
+
const docsDir = path_1.default.join(targetDir, 'docs');
|
|
1675
|
+
await fs_extra_1.default.ensureDir(docsDir);
|
|
1676
|
+
// Generate inline quickstart guide
|
|
1677
|
+
const quickstart = `# Quick Start
|
|
1678
|
+
|
|
1679
|
+
## Prerequisites
|
|
1680
|
+
- Node.js 20+
|
|
1681
|
+
- Docker Desktop
|
|
1682
|
+
- pnpm (recommended)
|
|
1683
|
+
|
|
1684
|
+
## Setup
|
|
1685
|
+
1. \`pnpm install\`
|
|
1686
|
+
2. \`docker-compose up -d\`
|
|
1687
|
+
3. \`pnpm db:push\`
|
|
1688
|
+
4. \`pnpm db:seed\`
|
|
1689
|
+
5. \`pnpm dev\`
|
|
1690
|
+
|
|
1691
|
+
## Default Login
|
|
1692
|
+
- Email: admin@example.com
|
|
1693
|
+
- Password: admin123
|
|
1694
|
+
|
|
1695
|
+
## CLI Commands
|
|
1696
|
+
- \`chimerai add <component>\` — Add features
|
|
1697
|
+
- \`chimerai setup <service>\` — Configure integrations
|
|
1698
|
+
- \`chimerai doctor\` — Health check
|
|
1699
|
+
- \`chimerai update --diff\` — Check for template updates
|
|
1700
|
+
`;
|
|
1701
|
+
await fs_extra_1.default.writeFile(path_1.default.join(docsDir, 'QUICKSTART.md'), quickstart);
|
|
1702
|
+
console.log(chalk_1.default.green(' ✓ docs/QUICKSTART.md'));
|
|
1703
|
+
}
|