@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,2126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Add Command - Add components to existing project
|
|
4
|
+
* REFACTORED: Uses inline template generators instead of fs.copy() (PHASE 4)
|
|
5
|
+
* Extended with modular AI-Service support (ai-chat, rag, guardrails, ai-tools)
|
|
6
|
+
*/
|
|
7
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
8
|
+
if (k2 === undefined) k2 = k;
|
|
9
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
10
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
11
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
12
|
+
}
|
|
13
|
+
Object.defineProperty(o, k2, desc);
|
|
14
|
+
}) : (function(o, m, k, k2) {
|
|
15
|
+
if (k2 === undefined) k2 = k;
|
|
16
|
+
o[k2] = m[k];
|
|
17
|
+
}));
|
|
18
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
19
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
20
|
+
}) : function(o, v) {
|
|
21
|
+
o["default"] = v;
|
|
22
|
+
});
|
|
23
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
24
|
+
var ownKeys = function(o) {
|
|
25
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
26
|
+
var ar = [];
|
|
27
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
28
|
+
return ar;
|
|
29
|
+
};
|
|
30
|
+
return ownKeys(o);
|
|
31
|
+
};
|
|
32
|
+
return function (mod) {
|
|
33
|
+
if (mod && mod.__esModule) return mod;
|
|
34
|
+
var result = {};
|
|
35
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
36
|
+
__setModuleDefault(result, mod);
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
})();
|
|
40
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
41
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
42
|
+
};
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.addCommand = addCommand;
|
|
45
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
46
|
+
const ora_1 = __importDefault(require("ora"));
|
|
47
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
48
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
49
|
+
const path_1 = __importDefault(require("path"));
|
|
50
|
+
const child_process_1 = require("child_process");
|
|
51
|
+
const crypto_1 = __importDefault(require("crypto"));
|
|
52
|
+
const templates = __importStar(require("../templates/index.js"));
|
|
53
|
+
const ai_service_tools_js_1 = require("../templates/ai-service-tools.js");
|
|
54
|
+
const utils_js_1 = require("../utils.js");
|
|
55
|
+
const schema_manager_js_1 = require("../schema-manager.js");
|
|
56
|
+
const scanner_js_1 = require("../scanner.js");
|
|
57
|
+
const license_js_1 = require("../license.js");
|
|
58
|
+
/**
|
|
59
|
+
* Component configurations - centralized (eliminates 11 separate add* functions)
|
|
60
|
+
* Reduces lines from ~900 to ~120
|
|
61
|
+
*/
|
|
62
|
+
const COMPONENT_CONFIGS = {
|
|
63
|
+
auth: {
|
|
64
|
+
name: 'auth',
|
|
65
|
+
displayName: 'Authentication System',
|
|
66
|
+
description: 'NextAuth.js integration with login, session, RBAC, and GDPR support',
|
|
67
|
+
files: [
|
|
68
|
+
{
|
|
69
|
+
generator: () => templates.generateNextAuthRoute(),
|
|
70
|
+
target: 'app/api/auth/[...nextauth]/route.ts',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
generator: () => templates.generateLoginPage(),
|
|
74
|
+
target: 'app/auth/signin/page.tsx',
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
generator: () => templates.generateSessionProvider(),
|
|
78
|
+
target: 'components/SessionProvider.tsx',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
generator: () => templates.generateAuthLib(),
|
|
82
|
+
target: 'lib/auth.ts',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
generator: () => templates.generateNextAuthTypes(),
|
|
86
|
+
target: 'types/next-auth.d.ts',
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
generator: () => templates.generateApiProtectionLib(),
|
|
90
|
+
target: 'lib/api-protection.ts',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
generator: () => templates.generateResolveAuth(),
|
|
94
|
+
target: 'lib/auth/resolve-auth.ts',
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
generator: () => templates.generateApiKeyAuthLib(),
|
|
98
|
+
target: 'lib/api-key-auth.ts',
|
|
99
|
+
},
|
|
100
|
+
// User profile route
|
|
101
|
+
{
|
|
102
|
+
generator: () => templates.generateUserProfileRoute(),
|
|
103
|
+
target: 'app/api/user/profile/route.ts',
|
|
104
|
+
},
|
|
105
|
+
// GDPR self-service routes
|
|
106
|
+
{
|
|
107
|
+
generator: () => templates.generateGdprDataExportRoute(),
|
|
108
|
+
target: 'app/api/user/data-export/route.ts',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
generator: () => templates.generateGdprAccountDeleteRoute(),
|
|
112
|
+
target: 'app/api/user/account/route.ts',
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
dependencies: {
|
|
116
|
+
'next-auth': '^4.24.10',
|
|
117
|
+
bcryptjs: '^3.0.0',
|
|
118
|
+
'@auth/prisma-adapter': '^2.11.1',
|
|
119
|
+
},
|
|
120
|
+
requiredUtils: [
|
|
121
|
+
{ generator: () => templates.generateAuditLogHelper(), target: 'lib/audit.ts' },
|
|
122
|
+
],
|
|
123
|
+
schemaExtension: `
|
|
124
|
+
model User {
|
|
125
|
+
id String @id @default(cuid())
|
|
126
|
+
name String?
|
|
127
|
+
email String? @unique
|
|
128
|
+
emailVerified DateTime?
|
|
129
|
+
image String?
|
|
130
|
+
password String?
|
|
131
|
+
createdAt DateTime @default(now())
|
|
132
|
+
updatedAt DateTime @updatedAt
|
|
133
|
+
accounts Account[]
|
|
134
|
+
sessions Session[]
|
|
135
|
+
apiKeys ApiKey[]
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
model Account {
|
|
139
|
+
id String @id @default(cuid())
|
|
140
|
+
userId String
|
|
141
|
+
type String
|
|
142
|
+
provider String
|
|
143
|
+
providerAccountId String
|
|
144
|
+
refresh_token String? @db.Text
|
|
145
|
+
access_token String? @db.Text
|
|
146
|
+
expires_at Int?
|
|
147
|
+
token_type String?
|
|
148
|
+
scope String?
|
|
149
|
+
id_token String? @db.Text
|
|
150
|
+
session_state String?
|
|
151
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
152
|
+
|
|
153
|
+
@@unique([provider, providerAccountId])
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
model Session {
|
|
157
|
+
id String @id @default(cuid())
|
|
158
|
+
sessionToken String @unique
|
|
159
|
+
userId String
|
|
160
|
+
expires DateTime
|
|
161
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
model VerificationToken {
|
|
165
|
+
identifier String
|
|
166
|
+
token String @unique
|
|
167
|
+
expires DateTime
|
|
168
|
+
|
|
169
|
+
@@unique([identifier, token])
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
model ApiKey {
|
|
173
|
+
id String @id @default(cuid())
|
|
174
|
+
name String
|
|
175
|
+
keyHash String @unique
|
|
176
|
+
userId String
|
|
177
|
+
scopes String[] @default(["chat"])
|
|
178
|
+
revoked Boolean @default(false)
|
|
179
|
+
lastUsedAt DateTime?
|
|
180
|
+
expiresAt DateTime?
|
|
181
|
+
createdAt DateTime @default(now())
|
|
182
|
+
updatedAt DateTime @updatedAt
|
|
183
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
184
|
+
|
|
185
|
+
@@index([keyHash])
|
|
186
|
+
}
|
|
187
|
+
`,
|
|
188
|
+
envVars: [
|
|
189
|
+
{
|
|
190
|
+
key: 'DATABASE_URL',
|
|
191
|
+
value: 'file:./dev.db',
|
|
192
|
+
comment: 'Database (SQLite for development, change to PostgreSQL for production)',
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
key: 'NEXTAUTH_SECRET',
|
|
196
|
+
value: () => crypto_1.default.randomBytes(32).toString('hex'),
|
|
197
|
+
comment: 'NextAuth.js secret (auto-generated)',
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
key: 'NEXTAUTH_URL',
|
|
201
|
+
value: 'http://localhost:__PORT_DETECT__',
|
|
202
|
+
comment: 'NextAuth.js base URL',
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
postAddMessage: `Authentication system added successfully!
|
|
206
|
+
|
|
207
|
+
â
NextAuth.js route + login page (app/auth/signin)
|
|
208
|
+
â
Auth lib (lib/auth.ts) + Session Provider component
|
|
209
|
+
â
Type declarations (types/next-auth.d.ts)
|
|
210
|
+
â
API protection + resolve-auth (dual-auth support)
|
|
211
|
+
â
User profile API + GDPR data export & account deletion
|
|
212
|
+
â
Seed script (prisma/seed.ts) â Demo user: admin@example.com / admin123
|
|
213
|
+
|
|
214
|
+
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
215
|
+
â IMPORTANT â Add SessionProvider to your root layout: â
|
|
216
|
+
â â
|
|
217
|
+
â // app/layout.tsx â
|
|
218
|
+
â import { SessionProvider } â
|
|
219
|
+
â from '@/components/SessionProvider'; â
|
|
220
|
+
â â
|
|
221
|
+
â // Wrap {children} inside <body>: â
|
|
222
|
+
â <SessionProvider>{children}</SessionProvider> â
|
|
223
|
+
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
224
|
+
|
|
225
|
+
Next steps:
|
|
226
|
+
1. Add <SessionProvider> to your root layout (see above)
|
|
227
|
+
2. pnpm install
|
|
228
|
+
3. npx prisma db push
|
|
229
|
+
4. npx prisma db seed
|
|
230
|
+
5. pnpm dev â Login: admin@example.com / admin123`,
|
|
231
|
+
},
|
|
232
|
+
'users-table': {
|
|
233
|
+
name: 'users-table',
|
|
234
|
+
displayName: 'Users Table',
|
|
235
|
+
description: 'User management data table with CRUD operations',
|
|
236
|
+
files: [
|
|
237
|
+
{
|
|
238
|
+
generator: () => templates.generateAdminUsersPage(),
|
|
239
|
+
target: 'app/admin/users/page.tsx',
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
generator: () => templates.generateAdminUsersRoute(),
|
|
243
|
+
target: 'app/api/admin/users/route.ts',
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
generator: () => templates.generateAdminUsersIdRoute(),
|
|
247
|
+
target: 'app/api/admin/users/[id]/route.ts',
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
postAddMessage: 'Users table component added',
|
|
251
|
+
},
|
|
252
|
+
'roles-table': {
|
|
253
|
+
name: 'roles-table',
|
|
254
|
+
displayName: 'Roles Table',
|
|
255
|
+
description: 'Role management table for RBAC system',
|
|
256
|
+
files: [
|
|
257
|
+
{
|
|
258
|
+
generator: () => templates.generateAdminRolesPage(),
|
|
259
|
+
target: 'app/admin/roles/page.tsx',
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
generator: () => templates.generateAdminRolesRoute(),
|
|
263
|
+
target: 'app/api/admin/roles/route.ts',
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
postAddMessage: 'Roles table component added',
|
|
267
|
+
},
|
|
268
|
+
'groups-table': {
|
|
269
|
+
name: 'groups-table',
|
|
270
|
+
displayName: 'Groups Table',
|
|
271
|
+
description: 'User groups/teams management table',
|
|
272
|
+
disabled: 'Not yet implemented â requires Entity-based RBAC (Group/GroupMember/GroupPermission models). Use admin-dashboard for Role-based access control.',
|
|
273
|
+
files: [],
|
|
274
|
+
postAddMessage: 'Groups table component added',
|
|
275
|
+
},
|
|
276
|
+
'permissions-table': {
|
|
277
|
+
name: 'permissions-table',
|
|
278
|
+
displayName: 'Permissions Table',
|
|
279
|
+
description: 'Permission management for roles',
|
|
280
|
+
disabled: 'Not yet implemented â CLI uses String-Array permissions on Role model. Use admin-dashboard for Role + Permission management.',
|
|
281
|
+
files: [],
|
|
282
|
+
postAddMessage: 'Permissions table component added',
|
|
283
|
+
},
|
|
284
|
+
'chat-ui': {
|
|
285
|
+
name: 'chat-ui',
|
|
286
|
+
displayName: 'Chat UI',
|
|
287
|
+
description: 'Full-featured chat interface with streaming, conversation management, and multi-provider support',
|
|
288
|
+
requiredUtils: [
|
|
289
|
+
{ generator: () => templates.generateUseAppNameHook(), target: 'lib/use-app-name.ts' },
|
|
290
|
+
],
|
|
291
|
+
files: [
|
|
292
|
+
// Hook
|
|
293
|
+
{
|
|
294
|
+
generator: () => templates.generateUseChatHook(),
|
|
295
|
+
target: 'components/chat/use-chat.ts',
|
|
296
|
+
},
|
|
297
|
+
// Components
|
|
298
|
+
{
|
|
299
|
+
generator: () => templates.generateChatMessage(),
|
|
300
|
+
target: 'components/chat/chat-message.tsx',
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
generator: () => templates.generateChatInput(),
|
|
304
|
+
target: 'components/chat/chat-input.tsx',
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
generator: () => templates.generateChatSidebar(),
|
|
308
|
+
target: 'components/chat/chat-sidebar.tsx',
|
|
309
|
+
},
|
|
310
|
+
{
|
|
311
|
+
generator: () => templates.generateModelSelector(),
|
|
312
|
+
target: 'components/chat/model-selector.tsx',
|
|
313
|
+
},
|
|
314
|
+
// Index barrel export
|
|
315
|
+
{
|
|
316
|
+
generator: () => `export { useChat } from './use-chat';
|
|
317
|
+
export { ChatMessage } from './chat-message';
|
|
318
|
+
export { ChatInput } from './chat-input';
|
|
319
|
+
export { ChatSidebar } from './chat-sidebar';
|
|
320
|
+
export { ModelSelector } from './model-selector';
|
|
321
|
+
export type { ChatMessageData, MessageActions, ConversationItem, ModelOption } from './use-chat';
|
|
322
|
+
`,
|
|
323
|
+
target: 'components/chat/index.ts',
|
|
324
|
+
},
|
|
325
|
+
// Example page â placed in _chimerai-examples/ so the user decides where to mount it
|
|
326
|
+
{
|
|
327
|
+
generator: () => templates.generateChatPage(),
|
|
328
|
+
target: '_chimerai-examples/chat-page.tsx',
|
|
329
|
+
},
|
|
330
|
+
// API Routes
|
|
331
|
+
{
|
|
332
|
+
generator: () => templates.generateChatStreamRouteWithPersistence(),
|
|
333
|
+
target: 'app/api/v1/chat/stream/route.ts',
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
generator: () => templates.generateConversationsRoute(),
|
|
337
|
+
target: 'app/api/conversations/route.ts',
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
generator: () => templates.generateConversationDetailRoute(),
|
|
341
|
+
target: 'app/api/conversations/[id]/route.ts',
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
generator: () => templates.generateModelsRoute(),
|
|
345
|
+
target: 'app/api/models/route.ts',
|
|
346
|
+
},
|
|
347
|
+
// Public v1 models route (dual-auth for external apps/widgets)
|
|
348
|
+
{
|
|
349
|
+
generator: () => templates.generateV1ModelsRoute(),
|
|
350
|
+
target: 'app/api/v1/models/route.ts',
|
|
351
|
+
},
|
|
352
|
+
{
|
|
353
|
+
generator: () => templates.generateProviderSyncRoute(),
|
|
354
|
+
target: 'app/api/providers/[id]/sync/route.ts',
|
|
355
|
+
},
|
|
356
|
+
// Infrastructure dependencies (prisma, encryption, dual-auth)
|
|
357
|
+
{
|
|
358
|
+
generator: () => templates.generatePrismaLib(),
|
|
359
|
+
target: 'lib/prisma.ts',
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
generator: () => templates.generateEncryptionLib(),
|
|
363
|
+
target: 'lib/encryption.ts',
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
generator: () => templates.generateResolveAuth(),
|
|
367
|
+
target: 'lib/auth/resolve-auth.ts',
|
|
368
|
+
},
|
|
369
|
+
{
|
|
370
|
+
generator: () => templates.generateApiKeyAuthLib(),
|
|
371
|
+
target: 'lib/api-key-auth.ts',
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
dependencies: {
|
|
375
|
+
'react-markdown': '^9.0.0',
|
|
376
|
+
'remark-gfm': '^4.0.0',
|
|
377
|
+
},
|
|
378
|
+
schemaExtension: `model Conversation {
|
|
379
|
+
id String @id @default(cuid())
|
|
380
|
+
userId String
|
|
381
|
+
title String @default("New Chat")
|
|
382
|
+
model String?
|
|
383
|
+
providerId String?
|
|
384
|
+
metadata Json?
|
|
385
|
+
archived Boolean @default(false)
|
|
386
|
+
createdAt DateTime @default(now())
|
|
387
|
+
updatedAt DateTime @updatedAt
|
|
388
|
+
|
|
389
|
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
390
|
+
provider Provider? @relation(fields: [providerId], references: [id], onDelete: SetNull)
|
|
391
|
+
messages Message[]
|
|
392
|
+
|
|
393
|
+
@@index([userId])
|
|
394
|
+
@@index([archived])
|
|
395
|
+
@@index([providerId])
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
model Message {
|
|
399
|
+
id String @id @default(cuid())
|
|
400
|
+
conversationId String
|
|
401
|
+
role String
|
|
402
|
+
content String @db.Text
|
|
403
|
+
model String?
|
|
404
|
+
tokens Int?
|
|
405
|
+
createdAt DateTime @default(now())
|
|
406
|
+
|
|
407
|
+
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
|
|
408
|
+
|
|
409
|
+
@@index([conversationId])
|
|
410
|
+
}`,
|
|
411
|
+
postAddMessage: `Chat UI added successfully!
|
|
412
|
+
|
|
413
|
+
â
Components: useChat hook, ChatMessage, ChatInput, ChatSidebar, ModelSelector
|
|
414
|
+
â
Routes: /api/v1/chat/stream, /api/conversations, /api/conversations/[id]
|
|
415
|
+
â
Features: Streaming, conversation persistence, multi-provider support
|
|
416
|
+
â
Example page: _chimerai-examples/chat-page.tsx
|
|
417
|
+
|
|
418
|
+
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
419
|
+
â Integration â add the chat to any page in your app: â
|
|
420
|
+
â â
|
|
421
|
+
â Option A: Copy the example page â
|
|
422
|
+
â cp _chimerai-examples/chat-page.tsx â
|
|
423
|
+
â app/chat/page.tsx â
|
|
424
|
+
â â
|
|
425
|
+
â Option B: Use individual components â
|
|
426
|
+
â import { useChat, ChatMessage, ChatInput } â
|
|
427
|
+
â from '@/components/chat'; â
|
|
428
|
+
â â
|
|
429
|
+
â Note: Pages using useSession() need SessionProvider â
|
|
430
|
+
â in a parent layout (see: chimerai add auth). â
|
|
431
|
+
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
432
|
+
|
|
433
|
+
Next steps:
|
|
434
|
+
1. Copy or adapt _chimerai-examples/chat-page.tsx into your app
|
|
435
|
+
2. pnpm install
|
|
436
|
+
3. npx prisma db push && npx prisma db seed
|
|
437
|
+
4. pnpm dev`,
|
|
438
|
+
},
|
|
439
|
+
'chat-widget': {
|
|
440
|
+
name: 'chat-widget',
|
|
441
|
+
displayName: 'Embeddable Chat Widget',
|
|
442
|
+
description: 'Self-contained JS chat widget + API-Key management for external integrations',
|
|
443
|
+
files: [
|
|
444
|
+
{
|
|
445
|
+
generator: () => templates.generateWidgetBundle(),
|
|
446
|
+
target: 'public/widget/chat.js',
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
generator: () => templates.generateWidgetLoader(),
|
|
450
|
+
target: 'public/widget/loader.js',
|
|
451
|
+
},
|
|
452
|
+
{
|
|
453
|
+
generator: () => templates.generateApiKeyManagementPage(),
|
|
454
|
+
target: 'app/(app)/settings/api-keys/page.tsx',
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
generator: () => templates.generateApiKeysRoute(),
|
|
458
|
+
target: 'app/api/v1/api-keys/route.ts',
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
generator: () => templates.generateApiKeyIdRoute(),
|
|
462
|
+
target: 'app/api/v1/api-keys/[id]/route.ts',
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
generator: () => templates.generateRateLimiter(),
|
|
466
|
+
target: 'lib/rate-limit.ts',
|
|
467
|
+
},
|
|
468
|
+
// Infrastructure dependencies (encryption for API keys, prisma for DB, auth helper)
|
|
469
|
+
{
|
|
470
|
+
generator: () => templates.generateEncryptionLib(),
|
|
471
|
+
target: 'lib/encryption.ts',
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
generator: () => templates.generatePrismaLib(),
|
|
475
|
+
target: 'lib/prisma.ts',
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
generator: () => templates.generateApiKeyAuthLib(),
|
|
479
|
+
target: 'lib/api-key-auth.ts',
|
|
480
|
+
},
|
|
481
|
+
],
|
|
482
|
+
requiredUtils: [
|
|
483
|
+
{
|
|
484
|
+
generator: () => templates.generateMiddleware(),
|
|
485
|
+
target: 'middleware.ts',
|
|
486
|
+
},
|
|
487
|
+
],
|
|
488
|
+
envVars: [
|
|
489
|
+
{
|
|
490
|
+
key: 'CORS_ALLOWED_ORIGINS',
|
|
491
|
+
value: '*',
|
|
492
|
+
comment: 'Allowed origins for widget CORS (comma-separated, e.g. http://localhost:3002)',
|
|
493
|
+
},
|
|
494
|
+
],
|
|
495
|
+
postAddMessage: `
|
|
496
|
+
â
Embeddable Chat Widget installed!
|
|
497
|
+
|
|
498
|
+
Files created:
|
|
499
|
+
⢠public/widget/chat.js â Self-contained Web Component (Shadow DOM)
|
|
500
|
+
⢠public/widget/loader.js â Async loader with auto-mount
|
|
501
|
+
⢠app/(app)/settings/api-keys/page.tsx â API Key Management UI
|
|
502
|
+
⢠app/api/v1/api-keys/ â API Key CRUD routes
|
|
503
|
+
⢠lib/rate-limit.ts â Rate limiter with API-key tier
|
|
504
|
+
⢠middleware.ts â CORS headers for cross-origin widget access
|
|
505
|
+
|
|
506
|
+
CORS (cross-origin embedding):
|
|
507
|
+
CORS_ALLOWED_ORIGINS has been added to .env (default: *).
|
|
508
|
+
For production, restrict to specific domains:
|
|
509
|
+
CORS_ALLOWED_ORIGINS=https://your-site.com,https://app.example.com
|
|
510
|
+
|
|
511
|
+
Next steps:
|
|
512
|
+
1. Navigate to /settings/api-keys to create an API key
|
|
513
|
+
2. Use the embed code to add the chat to any external website:
|
|
514
|
+
|
|
515
|
+
<script src="https://your-app.com/widget/chat.js"></script>
|
|
516
|
+
<div id="chat"></div>
|
|
517
|
+
<script>
|
|
518
|
+
ChimerAI.mount('#chat', { apiKey: 'sk_live_...', theme: 'auto' });
|
|
519
|
+
</script>
|
|
520
|
+
`,
|
|
521
|
+
},
|
|
522
|
+
'model-providers': {
|
|
523
|
+
name: 'model-providers',
|
|
524
|
+
displayName: 'Model Providers',
|
|
525
|
+
description: 'AI model provider management (page + CRUD + Internal API + test)',
|
|
526
|
+
requiredUtils: [
|
|
527
|
+
{ generator: () => templates.generateAuditLogHelper(), target: 'lib/audit.ts' },
|
|
528
|
+
],
|
|
529
|
+
files: [
|
|
530
|
+
// Example page â placed in _chimerai-examples/ so the user decides where to mount it
|
|
531
|
+
{
|
|
532
|
+
generator: () => templates.generateModelProvidersPage(),
|
|
533
|
+
target: '_chimerai-examples/providers-page.tsx',
|
|
534
|
+
},
|
|
535
|
+
// Provider CRUD route â /api/providers
|
|
536
|
+
{
|
|
537
|
+
generator: () => templates.generateProviderCrudRoute(),
|
|
538
|
+
target: 'app/api/providers/route.ts',
|
|
539
|
+
},
|
|
540
|
+
// Provider [id] route â /api/providers/[id]
|
|
541
|
+
{
|
|
542
|
+
generator: () => templates.generateProviderIdRoute(),
|
|
543
|
+
target: 'app/api/providers/[id]/route.ts',
|
|
544
|
+
},
|
|
545
|
+
// Provider test route â /api/providers/[id]/test
|
|
546
|
+
{
|
|
547
|
+
generator: () => templates.generateProviderTestRoute(),
|
|
548
|
+
target: 'app/api/providers/[id]/test/route.ts',
|
|
549
|
+
},
|
|
550
|
+
// Provider sync route â /api/providers/[id]/sync
|
|
551
|
+
{
|
|
552
|
+
generator: () => templates.generateProviderSyncRoute(),
|
|
553
|
+
target: 'app/api/providers/[id]/sync/route.ts',
|
|
554
|
+
},
|
|
555
|
+
// Internal providers route â /api/internal/providers
|
|
556
|
+
{
|
|
557
|
+
generator: () => templates.generateInternalProvidersRoute(),
|
|
558
|
+
target: 'app/api/internal/providers/route.ts',
|
|
559
|
+
},
|
|
560
|
+
// Internal provider [id] route â /api/internal/providers/[id]
|
|
561
|
+
{
|
|
562
|
+
generator: () => templates.generateInternalProviderIdRoute(),
|
|
563
|
+
target: 'app/api/internal/providers/[id]/route.ts',
|
|
564
|
+
},
|
|
565
|
+
// Internal usage route â /api/internal/providers/[id]/usage
|
|
566
|
+
{
|
|
567
|
+
generator: () => templates.generateInternalProviderUsageRoute(),
|
|
568
|
+
target: 'app/api/internal/providers/[id]/usage/route.ts',
|
|
569
|
+
},
|
|
570
|
+
// Notify provider change utility
|
|
571
|
+
{
|
|
572
|
+
generator: () => templates.generateNotifyProviderChangeLib(),
|
|
573
|
+
target: 'lib/notify-provider-change.ts',
|
|
574
|
+
},
|
|
575
|
+
// Infrastructure dependencies
|
|
576
|
+
{
|
|
577
|
+
generator: () => templates.generateEncryptionLib(),
|
|
578
|
+
target: 'lib/encryption.ts',
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
generator: () => templates.generatePrismaLib(),
|
|
582
|
+
target: 'lib/prisma.ts',
|
|
583
|
+
},
|
|
584
|
+
],
|
|
585
|
+
schemaExtension: `model Provider {
|
|
586
|
+
id String @id @default(cuid())
|
|
587
|
+
name String
|
|
588
|
+
type String
|
|
589
|
+
description String?
|
|
590
|
+
baseUrl String?
|
|
591
|
+
apiKey String? @db.Text
|
|
592
|
+
config Json @default("{}")
|
|
593
|
+
status String @default("active")
|
|
594
|
+
isDefault Boolean @default(false)
|
|
595
|
+
priority Int @default(0)
|
|
596
|
+
tags String[]
|
|
597
|
+
createdAt DateTime @default(now())
|
|
598
|
+
updatedAt DateTime @updatedAt
|
|
599
|
+
createdBy String?
|
|
600
|
+
|
|
601
|
+
models Model[]
|
|
602
|
+
health ProviderHealth?
|
|
603
|
+
apiUsage ApiUsage[]
|
|
604
|
+
|
|
605
|
+
@@index([type])
|
|
606
|
+
@@index([status])
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
model Model {
|
|
610
|
+
id String @id @default(cuid())
|
|
611
|
+
providerId String
|
|
612
|
+
modelId String
|
|
613
|
+
name String
|
|
614
|
+
description String?
|
|
615
|
+
capabilities String[]
|
|
616
|
+
contextWindow Int @default(4096)
|
|
617
|
+
maxOutputTokens Int?
|
|
618
|
+
inputCost Float @default(0)
|
|
619
|
+
outputCost Float @default(0)
|
|
620
|
+
isAvailable Boolean @default(true)
|
|
621
|
+
isDeprecated Boolean @default(false)
|
|
622
|
+
|
|
623
|
+
provider Provider @relation(fields: [providerId], references: [id], onDelete: Cascade)
|
|
624
|
+
|
|
625
|
+
@@unique([providerId, modelId])
|
|
626
|
+
@@index([providerId])
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
model ProviderHealth {
|
|
630
|
+
id String @id @default(cuid())
|
|
631
|
+
providerId String @unique
|
|
632
|
+
status String @default("unknown")
|
|
633
|
+
responseTime Int?
|
|
634
|
+
lastCheck DateTime @default(now())
|
|
635
|
+
errorMessage String?
|
|
636
|
+
modelsAvailable Int @default(0)
|
|
637
|
+
chatAvailable Boolean @default(false)
|
|
638
|
+
embeddingAvailable Boolean @default(false)
|
|
639
|
+
apiKeyValid Boolean @default(false)
|
|
640
|
+
|
|
641
|
+
provider Provider @relation(fields: [providerId], references: [id], onDelete: Cascade)
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
model ApiUsage {
|
|
645
|
+
id String @id @default(cuid())
|
|
646
|
+
userId String
|
|
647
|
+
providerId String?
|
|
648
|
+
model String
|
|
649
|
+
endpoint String
|
|
650
|
+
promptTokens Int @default(0)
|
|
651
|
+
completionTokens Int @default(0)
|
|
652
|
+
totalTokens Int @default(0)
|
|
653
|
+
tokensUsed Int @default(0)
|
|
654
|
+
creditsUsed Int @default(0)
|
|
655
|
+
cost Float @default(0)
|
|
656
|
+
success Boolean @default(true)
|
|
657
|
+
errorMessage String?
|
|
658
|
+
responseTime Int @default(0)
|
|
659
|
+
createdAt DateTime @default(now())
|
|
660
|
+
|
|
661
|
+
provider Provider? @relation(fields: [providerId], references: [id], onDelete: SetNull)
|
|
662
|
+
|
|
663
|
+
@@index([userId])
|
|
664
|
+
@@index([providerId])
|
|
665
|
+
@@index([createdAt])
|
|
666
|
+
}
|
|
667
|
+
`,
|
|
668
|
+
envVars: [
|
|
669
|
+
{
|
|
670
|
+
key: 'PROVIDER_ENCRYPTION_KEY',
|
|
671
|
+
value: () => crypto_1.default.randomBytes(32).toString('hex'),
|
|
672
|
+
comment: 'Encryption key for provider API keys (auto-generated)',
|
|
673
|
+
},
|
|
674
|
+
],
|
|
675
|
+
postAddMessage: `Model providers added successfully!
|
|
676
|
+
|
|
677
|
+
â
Provider CRUD + [id] + test + sync routes
|
|
678
|
+
â
Internal provider routes (AI-Service â Frontend)
|
|
679
|
+
â
Encryption lib + notify-provider-change utility
|
|
680
|
+
â
Provider seed script (prisma/seed-providers.ts) â OpenAI defaults
|
|
681
|
+
â
Example page: _chimerai-examples/providers-page.tsx
|
|
682
|
+
|
|
683
|
+
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
684
|
+
â Integration â add provider management to your app: â
|
|
685
|
+
â â
|
|
686
|
+
â cp _chimerai-examples/providers-page.tsx â
|
|
687
|
+
â app/dashboard/providers/page.tsx â
|
|
688
|
+
â â
|
|
689
|
+
â Note: The page uses useSession() and needs â
|
|
690
|
+
â SessionProvider in a parent layout. â
|
|
691
|
+
âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
692
|
+
|
|
693
|
+
Next steps:
|
|
694
|
+
1. Set OPENAI_API_KEY in .env (or add provider manually later)
|
|
695
|
+
2. Copy or adapt _chimerai-examples/providers-page.tsx into your app
|
|
696
|
+
3. npx prisma db push (if not done yet)
|
|
697
|
+
4. npx prisma db seed`,
|
|
698
|
+
},
|
|
699
|
+
'prompt-management': {
|
|
700
|
+
name: 'prompt-management',
|
|
701
|
+
displayName: 'Prompt Management',
|
|
702
|
+
description: 'System prompt and instruction management',
|
|
703
|
+
files: [
|
|
704
|
+
{
|
|
705
|
+
generator: () => templates.generatePromptManagementPage(),
|
|
706
|
+
target: 'app/dashboard/prompts/page.tsx',
|
|
707
|
+
},
|
|
708
|
+
// Prompts API routes (CRUD)
|
|
709
|
+
{
|
|
710
|
+
generator: () => templates.generatePromptsRoute(),
|
|
711
|
+
target: 'app/api/prompts/route.ts',
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
generator: () => templates.generatePromptsIdRoute(),
|
|
715
|
+
target: 'app/api/prompts/[id]/route.ts',
|
|
716
|
+
},
|
|
717
|
+
{
|
|
718
|
+
generator: () => templates.generatePromptsSetDefaultRoute(),
|
|
719
|
+
target: 'app/api/prompts/[id]/set-default/route.ts',
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
generator: () => templates.generatePromptSelector(),
|
|
723
|
+
target: 'components/chat/prompt-selector.tsx',
|
|
724
|
+
},
|
|
725
|
+
],
|
|
726
|
+
postAddMessage: `Prompt management added successfully!
|
|
727
|
+
|
|
728
|
+
â
Prompt Management page (app/dashboard/prompts)
|
|
729
|
+
â
Prompts API routes (/api/prompts, /api/prompts/[id], /api/prompts/[id]/set-default)
|
|
730
|
+
â
PromptSelector component (components/chat/prompt-selector.tsx)
|
|
731
|
+
|
|
732
|
+
Next steps:
|
|
733
|
+
1. Run: npx prisma db push (to apply PromptTemplate schema)
|
|
734
|
+
2. Add seed data: npx prisma db seed
|
|
735
|
+
3. Navigate to /dashboard/prompts to manage system prompts
|
|
736
|
+
4. Chat integration: pass promptId in the chat request body to use a specific template
|
|
737
|
+
Or set a template as Default â it will be applied automatically to all new chats`,
|
|
738
|
+
},
|
|
739
|
+
rag: {
|
|
740
|
+
name: 'rag',
|
|
741
|
+
displayName: 'RAG (Retrieval Augmented Generation)',
|
|
742
|
+
description: 'Document upload, indexing, and AI-powered Q&A via AI Service',
|
|
743
|
+
files: [
|
|
744
|
+
// RAG library (AI Service client helper)
|
|
745
|
+
{
|
|
746
|
+
generator: () => templates.generateRagLib(),
|
|
747
|
+
target: 'lib/rag.ts',
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
generator: () => templates.generateRagQueryRoute(),
|
|
751
|
+
target: 'app/api/rag/query/route.ts',
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
generator: () => templates.generateRagUploadRoute(),
|
|
755
|
+
target: 'app/api/rag/route.ts',
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
generator: () => templates.generateRagPage(),
|
|
759
|
+
target: 'app/rag/page.tsx',
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
generator: () => templates.generateRagStatsRoute(),
|
|
763
|
+
target: 'app/api/rag/stats/route.ts',
|
|
764
|
+
},
|
|
765
|
+
{
|
|
766
|
+
generator: () => templates.generateRagClearRoute(),
|
|
767
|
+
target: 'app/api/rag/clear/route.ts',
|
|
768
|
+
},
|
|
769
|
+
],
|
|
770
|
+
// No SDK dependencies â uses AI Service via fetch()
|
|
771
|
+
postAddMessage: 'RAG system added (uses AI Service for embeddings + vector search)',
|
|
772
|
+
},
|
|
773
|
+
billing: {
|
|
774
|
+
name: 'billing',
|
|
775
|
+
displayName: 'Billing System',
|
|
776
|
+
description: 'Stripe integration for subscriptions',
|
|
777
|
+
files: [
|
|
778
|
+
{
|
|
779
|
+
generator: () => templates.generateBillingPage(),
|
|
780
|
+
target: 'app/billing/page.tsx',
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
generator: () => templates.generateStripeLib(),
|
|
784
|
+
target: 'lib/stripe.ts',
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
generator: () => templates.generateCheckoutRoute(),
|
|
788
|
+
target: 'app/api/billing/checkout/route.ts',
|
|
789
|
+
},
|
|
790
|
+
{
|
|
791
|
+
generator: () => templates.generatePortalRoute(),
|
|
792
|
+
target: 'app/api/billing/portal/route.ts',
|
|
793
|
+
},
|
|
794
|
+
{
|
|
795
|
+
generator: () => templates.generateSubscriptionRoute(),
|
|
796
|
+
target: 'app/api/billing/subscription/route.ts',
|
|
797
|
+
},
|
|
798
|
+
{
|
|
799
|
+
generator: () => templates.generateStripeWebhookRoute(),
|
|
800
|
+
target: 'app/api/webhooks/stripe/route.ts',
|
|
801
|
+
},
|
|
802
|
+
],
|
|
803
|
+
dependencies: {
|
|
804
|
+
stripe: '^14.0.0',
|
|
805
|
+
},
|
|
806
|
+
envVars: [
|
|
807
|
+
{
|
|
808
|
+
key: 'STRIPE_SECRET_KEY',
|
|
809
|
+
value: 'sk_test_',
|
|
810
|
+
comment: 'Stripe secret key (get from https://dashboard.stripe.com/apikeys)',
|
|
811
|
+
},
|
|
812
|
+
{
|
|
813
|
+
key: 'STRIPE_WEBHOOK_SECRET',
|
|
814
|
+
value: 'whsec_',
|
|
815
|
+
comment: 'Stripe webhook signing secret (get from Stripe Dashboard â Webhooks)',
|
|
816
|
+
},
|
|
817
|
+
],
|
|
818
|
+
postAddMessage: 'Billing system added successfully',
|
|
819
|
+
},
|
|
820
|
+
gdpr: {
|
|
821
|
+
name: 'gdpr',
|
|
822
|
+
displayName: 'GDPR Compliance',
|
|
823
|
+
description: 'Consent management, data export and account deletion (right to erasure)',
|
|
824
|
+
files: [
|
|
825
|
+
{
|
|
826
|
+
generator: () => templates.generateGdprPage(),
|
|
827
|
+
target: 'app/gdpr/page.tsx',
|
|
828
|
+
},
|
|
829
|
+
{
|
|
830
|
+
generator: () => templates.generateGdprLib(),
|
|
831
|
+
target: 'lib/gdpr.ts',
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
generator: () => templates.generateGdprConsentRoute(),
|
|
835
|
+
target: 'app/api/gdpr/consent/route.ts',
|
|
836
|
+
},
|
|
837
|
+
{
|
|
838
|
+
generator: () => templates.generateGdprDataExportRoute(),
|
|
839
|
+
target: 'app/api/gdpr/export/route.ts',
|
|
840
|
+
},
|
|
841
|
+
{
|
|
842
|
+
generator: () => templates.generateGdprAccountDeleteRoute(),
|
|
843
|
+
target: 'app/api/gdpr/delete/route.ts',
|
|
844
|
+
},
|
|
845
|
+
],
|
|
846
|
+
schemaExtension: templates.GDPR_SCHEMA_EXTENSION,
|
|
847
|
+
postAddMessage: 'â
GDPR portal at /gdpr\nâ
API routes: /api/gdpr/export, /api/gdpr/delete, /api/gdpr/consent\nâ
ConsentLog model added to Prisma schema',
|
|
848
|
+
},
|
|
849
|
+
mfa: {
|
|
850
|
+
name: 'mfa',
|
|
851
|
+
displayName: 'Multi-Factor Authentication',
|
|
852
|
+
description: 'TOTP-based 2FA (Google Authenticator, Authy) with QR code setup',
|
|
853
|
+
files: [
|
|
854
|
+
{
|
|
855
|
+
generator: () => templates.generateMfaPage(),
|
|
856
|
+
target: 'app/(app)/settings/mfa/page.tsx',
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
generator: () => templates.generateMfaLib(),
|
|
860
|
+
target: 'lib/mfa.ts',
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
generator: () => templates.generateMfaSetupRoute(),
|
|
864
|
+
target: 'app/api/mfa/setup/route.ts',
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
generator: () => templates.generateMfaVerifyRoute(),
|
|
868
|
+
target: 'app/api/mfa/verify/route.ts',
|
|
869
|
+
},
|
|
870
|
+
{
|
|
871
|
+
generator: () => templates.generateMfaDisableRoute(),
|
|
872
|
+
target: 'app/api/mfa/disable/route.ts',
|
|
873
|
+
},
|
|
874
|
+
],
|
|
875
|
+
dependencies: {
|
|
876
|
+
otpauth: '^9.0.0',
|
|
877
|
+
qrcode: '^1.5.3',
|
|
878
|
+
'@types/qrcode': '^1.5.5',
|
|
879
|
+
},
|
|
880
|
+
schemaExtension: templates.MFA_SCHEMA_EXTENSION,
|
|
881
|
+
postAddMessage: 'â
MFA setup page at /settings/mfa\nâ
API routes: /api/mfa/setup, /api/mfa/verify, /api/mfa/disable\nâ
MfaBackupCode model added to Prisma schema (SQLite + PostgreSQL compatible)\nâ ď¸ Add mfaSecret/mfaEnabled fields to User model manually or run: npx prisma migrate dev --name add-mfa',
|
|
882
|
+
},
|
|
883
|
+
'audit-log': {
|
|
884
|
+
name: 'audit-log',
|
|
885
|
+
displayName: 'Audit Log',
|
|
886
|
+
description: 'Compliance audit logging â track all admin and user actions',
|
|
887
|
+
files: [
|
|
888
|
+
{
|
|
889
|
+
generator: () => templates.generateAdminLogsPage(),
|
|
890
|
+
target: 'app/admin/audit-logs/page.tsx',
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
generator: () => templates.generateAuditLogHelper(),
|
|
894
|
+
target: 'lib/audit-log.ts',
|
|
895
|
+
},
|
|
896
|
+
{
|
|
897
|
+
generator: () => templates.generateAuditLogRoute(),
|
|
898
|
+
target: 'app/api/admin/audit-logs/route.ts',
|
|
899
|
+
},
|
|
900
|
+
],
|
|
901
|
+
postAddMessage: 'â
Audit log page at /admin/audit-logs\nâ
lib/audit-log.ts â call logAction() anywhere\nâ
API route: /api/admin/audit-logs\nâ ď¸ Requires AuditLog model in Prisma schema (included in admin-dashboard)',
|
|
902
|
+
},
|
|
903
|
+
'admin-dashboard': {
|
|
904
|
+
name: 'admin-dashboard',
|
|
905
|
+
displayName: 'Admin Dashboard',
|
|
906
|
+
description: 'Complete admin panel with user/role management, settings, audit logs, and RBAC',
|
|
907
|
+
files: [
|
|
908
|
+
// Admin layout with session + admin role guard
|
|
909
|
+
{
|
|
910
|
+
generator: () => templates.generateAdminLayout(),
|
|
911
|
+
target: 'app/admin/layout.tsx',
|
|
912
|
+
},
|
|
913
|
+
// Admin dashboard page
|
|
914
|
+
{
|
|
915
|
+
generator: () => templates.generateAdminDashboardPage(),
|
|
916
|
+
target: 'app/admin/page.tsx',
|
|
917
|
+
},
|
|
918
|
+
// Admin users page
|
|
919
|
+
{
|
|
920
|
+
generator: () => templates.generateAdminUsersPage(),
|
|
921
|
+
target: 'app/admin/users/page.tsx',
|
|
922
|
+
},
|
|
923
|
+
// Admin roles page
|
|
924
|
+
{
|
|
925
|
+
generator: () => templates.generateAdminRolesPage(),
|
|
926
|
+
target: 'app/admin/roles/page.tsx',
|
|
927
|
+
},
|
|
928
|
+
// Admin settings page
|
|
929
|
+
{
|
|
930
|
+
generator: () => templates.generateAdminSettingsPage(),
|
|
931
|
+
target: 'app/admin/settings/page.tsx',
|
|
932
|
+
},
|
|
933
|
+
// Admin audit logs page
|
|
934
|
+
{
|
|
935
|
+
generator: () => templates.generateAdminLogsPage(),
|
|
936
|
+
target: 'app/admin/audit-logs/page.tsx',
|
|
937
|
+
},
|
|
938
|
+
// Audit log helper utility
|
|
939
|
+
{
|
|
940
|
+
generator: () => templates.generateAuditLogHelper(),
|
|
941
|
+
target: 'lib/audit-log.ts',
|
|
942
|
+
},
|
|
943
|
+
// Admin API routes â users CRUD
|
|
944
|
+
{
|
|
945
|
+
generator: () => templates.generateAdminUsersRoute(),
|
|
946
|
+
target: 'app/api/admin/users/route.ts',
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
generator: () => templates.generateAdminUsersIdRoute(),
|
|
950
|
+
target: 'app/api/admin/users/[id]/route.ts',
|
|
951
|
+
},
|
|
952
|
+
// Admin API routes â roles CRUD
|
|
953
|
+
{
|
|
954
|
+
generator: () => templates.generateAdminRolesRoute(),
|
|
955
|
+
target: 'app/api/admin/roles/route.ts',
|
|
956
|
+
},
|
|
957
|
+
{
|
|
958
|
+
generator: () => templates.generateAdminRolesIdRoute(),
|
|
959
|
+
target: 'app/api/admin/roles/[id]/route.ts',
|
|
960
|
+
},
|
|
961
|
+
// Admin API routes â settings + audit logs
|
|
962
|
+
{
|
|
963
|
+
generator: () => templates.generateAdminSettingsRoute(),
|
|
964
|
+
target: 'app/api/admin/settings/route.ts',
|
|
965
|
+
},
|
|
966
|
+
{
|
|
967
|
+
generator: () => templates.generateAuditLogRoute(),
|
|
968
|
+
target: 'app/api/admin/audit-logs/route.ts',
|
|
969
|
+
},
|
|
970
|
+
// Public app-settings API route (used by useAppName hook)
|
|
971
|
+
{
|
|
972
|
+
generator: () => templates.generateAppSettingsRoute(),
|
|
973
|
+
target: 'app/api/app-settings/route.ts',
|
|
974
|
+
},
|
|
975
|
+
// useAppName client hook
|
|
976
|
+
{
|
|
977
|
+
generator: () => templates.generateUseAppNameHook(),
|
|
978
|
+
target: 'lib/use-app-name.ts',
|
|
979
|
+
},
|
|
980
|
+
// RBAC Permission utilities
|
|
981
|
+
{
|
|
982
|
+
generator: () => templates.generatePermissionsLib(),
|
|
983
|
+
target: 'lib/permissions.ts',
|
|
984
|
+
},
|
|
985
|
+
{
|
|
986
|
+
generator: () => templates.generateRequirePermissionLib(),
|
|
987
|
+
target: 'lib/auth/require-permission.ts',
|
|
988
|
+
},
|
|
989
|
+
],
|
|
990
|
+
postAddMessage: `Admin dashboard added successfully!
|
|
991
|
+
|
|
992
|
+
â
Admin layout with role guard
|
|
993
|
+
â
Pages: dashboard, users, roles, settings, audit logs
|
|
994
|
+
â
API routes: users CRUD, roles CRUD, settings, audit-logs, app-settings
|
|
995
|
+
â
RBAC: permissions lib + require-permission helper
|
|
996
|
+
â
Utilities: audit log helper, useAppName hook
|
|
997
|
+
|
|
998
|
+
Next steps:
|
|
999
|
+
1. Ensure auth component is installed (required for admin)
|
|
1000
|
+
2. Run prisma migrate to create Role, UserRole, AuditLog models
|
|
1001
|
+
3. Navigate to /admin to access the admin panel`,
|
|
1002
|
+
},
|
|
1003
|
+
};
|
|
1004
|
+
/**
|
|
1005
|
+
* Detect the dev-server port from the project's package.json scripts.
|
|
1006
|
+
* Scans "dev" script for `--port XXXX` or `-p XXXX`. Falls back to 3000.
|
|
1007
|
+
*/
|
|
1008
|
+
async function detectPort(targetDir) {
|
|
1009
|
+
try {
|
|
1010
|
+
const pkgPath = path_1.default.join(targetDir, 'package.json');
|
|
1011
|
+
if (await fs_extra_1.default.pathExists(pkgPath)) {
|
|
1012
|
+
const pkg = await fs_extra_1.default.readJson(pkgPath);
|
|
1013
|
+
const devScript = pkg?.scripts?.dev ?? '';
|
|
1014
|
+
// Match --port 3001 or -p 3001 (with or without =)
|
|
1015
|
+
const match = devScript.match(/(?:--port|-p)[=\s]+(\d+)/);
|
|
1016
|
+
if (match)
|
|
1017
|
+
return parseInt(match[1], 10);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
catch {
|
|
1021
|
+
// ignore parse errors
|
|
1022
|
+
}
|
|
1023
|
+
return 3000;
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Generate prisma/seed.ts â creates a demo admin user.
|
|
1027
|
+
* Minimal: only touches User model (no Role, UserRole, CreditBalance â those
|
|
1028
|
+
* don't exist in foreign-project schemas installed via `chimerai add`).
|
|
1029
|
+
*/
|
|
1030
|
+
function generateAuthSeed() {
|
|
1031
|
+
return `import { PrismaClient } from '@prisma/client';
|
|
1032
|
+
import { hash } from 'bcryptjs';
|
|
1033
|
+
|
|
1034
|
+
const prisma = new PrismaClient();
|
|
1035
|
+
|
|
1036
|
+
async function main() {
|
|
1037
|
+
const email = 'admin@example.com';
|
|
1038
|
+
const password = 'admin123';
|
|
1039
|
+
|
|
1040
|
+
const existing = await prisma.user.findUnique({ where: { email } });
|
|
1041
|
+
if (existing) {
|
|
1042
|
+
console.log(\`â
User \${email} already exists â skipping.\`);
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const hashed = await hash(password, 12);
|
|
1047
|
+
await prisma.user.create({
|
|
1048
|
+
data: {
|
|
1049
|
+
name: 'Admin',
|
|
1050
|
+
email,
|
|
1051
|
+
password: hashed,
|
|
1052
|
+
},
|
|
1053
|
+
});
|
|
1054
|
+
console.log(\`â
Demo user created: \${email} / \${password}\`);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
main()
|
|
1058
|
+
.catch((e) => {
|
|
1059
|
+
console.error(e);
|
|
1060
|
+
process.exit(1);
|
|
1061
|
+
})
|
|
1062
|
+
.finally(() => prisma.$disconnect());
|
|
1063
|
+
`;
|
|
1064
|
+
}
|
|
1065
|
+
/**
|
|
1066
|
+
* Generate prisma/seed-providers.ts â seeds an OpenAI provider + default models.
|
|
1067
|
+
* Uses findFirst + create (Provider.name is NOT @unique in foreign schemas).
|
|
1068
|
+
*/
|
|
1069
|
+
function generateProviderSeed() {
|
|
1070
|
+
return `import { PrismaClient } from '@prisma/client';
|
|
1071
|
+
|
|
1072
|
+
const prisma = new PrismaClient();
|
|
1073
|
+
|
|
1074
|
+
async function main() {
|
|
1075
|
+
// Check if OpenAI provider already exists
|
|
1076
|
+
const existing = await prisma.provider.findFirst({ where: { name: 'OpenAI' } });
|
|
1077
|
+
if (existing) {
|
|
1078
|
+
console.log('â
OpenAI provider already exists â skipping.');
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
const apiKey = process.env.OPENAI_API_KEY ?? '';
|
|
1083
|
+
const isActive = apiKey.length > 0;
|
|
1084
|
+
const provider = await prisma.provider.create({
|
|
1085
|
+
data: {
|
|
1086
|
+
name: 'OpenAI',
|
|
1087
|
+
type: 'openai',
|
|
1088
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
1089
|
+
apiKey,
|
|
1090
|
+
config: { defaultModel: 'gpt-4o-mini' },
|
|
1091
|
+
status: isActive ? 'active' : 'inactive',
|
|
1092
|
+
tags: JSON.stringify(['llm', 'chat', 'embeddings']),
|
|
1093
|
+
},
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
// Seed default models
|
|
1097
|
+
const models = [
|
|
1098
|
+
{ name: 'GPT-4o', modelId: 'gpt-4o', contextWindow: 128000, capabilities: JSON.stringify(['chat', 'vision']) },
|
|
1099
|
+
{ name: 'GPT-4o Mini', modelId: 'gpt-4o-mini', contextWindow: 128000, capabilities: JSON.stringify(['chat']) },
|
|
1100
|
+
];
|
|
1101
|
+
for (const m of models) {
|
|
1102
|
+
await prisma.model.create({
|
|
1103
|
+
data: {
|
|
1104
|
+
...m,
|
|
1105
|
+
providerId: provider.id,
|
|
1106
|
+
isAvailable: isActive,
|
|
1107
|
+
},
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
console.log(\`â
OpenAI provider + \${models.length} models seeded.\`);
|
|
1112
|
+
if (!apiKey) {
|
|
1113
|
+
console.log('â ď¸ No OPENAI_API_KEY found â provider created but inactive.');
|
|
1114
|
+
console.log(' Set OPENAI_API_KEY in .env and re-run: npx prisma db seed');
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
main()
|
|
1119
|
+
.catch((e) => {
|
|
1120
|
+
console.error(e);
|
|
1121
|
+
process.exit(1);
|
|
1122
|
+
})
|
|
1123
|
+
.finally(() => prisma.$disconnect());
|
|
1124
|
+
`;
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Transform a PostgreSQL-native Prisma schema to be SQLite-compatible.
|
|
1128
|
+
* Applied as a post-processing step so templates stay PostgreSQL-first.
|
|
1129
|
+
*
|
|
1130
|
+
* @db.Text â removed (SQLite String is unbounded)
|
|
1131
|
+
* Json / Json? â String / String?
|
|
1132
|
+
* String[] â String (app code uses JSON.parse)
|
|
1133
|
+
* @default("{}") â @default("{}") (kept, works as String default)
|
|
1134
|
+
* @default(["..."]) â @default("...") (array default â plain string)
|
|
1135
|
+
*/
|
|
1136
|
+
function transformSchemaForSqlite(schema) {
|
|
1137
|
+
let s = schema;
|
|
1138
|
+
// 1. Remove @db.Text annotations
|
|
1139
|
+
s = s.replace(/\s+@db\.Text/g, '');
|
|
1140
|
+
// 2. Convert Json fields â String (preserve optional ?)
|
|
1141
|
+
// Json? â String?
|
|
1142
|
+
// Json â String
|
|
1143
|
+
s = s.replace(/(\s+)Json(\??)(\s+)/g, '$1String$2$3');
|
|
1144
|
+
// 3. Convert String[] â String (arrays not supported)
|
|
1145
|
+
s = s.replace(/String\[\]/g, 'String');
|
|
1146
|
+
// 4. Fix array defaults: @default(["chat"]) â @default("chat")
|
|
1147
|
+
s = s.replace(/@default\(\[([^\]]*)\]\)/g, (_match, inner) => {
|
|
1148
|
+
// inner is e.g. '"chat"' â strip quotes, join if multiple
|
|
1149
|
+
const items = inner
|
|
1150
|
+
.replace(/"/g, '')
|
|
1151
|
+
.split(',')
|
|
1152
|
+
.map((x) => x.trim())
|
|
1153
|
+
.join(',');
|
|
1154
|
+
return `@default("${items}")`;
|
|
1155
|
+
});
|
|
1156
|
+
return s;
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Generic component addition function (PHASE 4 refactoring)
|
|
1160
|
+
* Uses inline template generators â no fs.copy(), no dependency on templates/ folder
|
|
1161
|
+
*/
|
|
1162
|
+
async function addComponent(config, targetDir) {
|
|
1163
|
+
const spinner = (0, ora_1.default)(`Adding ${config.displayName}...`).start();
|
|
1164
|
+
try {
|
|
1165
|
+
// 1. Generate and write all required files
|
|
1166
|
+
// Detect if project uses (app) route group for pages
|
|
1167
|
+
const hasAppGroup = await fs_extra_1.default.pathExists(path_1.default.join(targetDir, 'app/(app)'));
|
|
1168
|
+
for (const file of config.files) {
|
|
1169
|
+
let target = file.target;
|
|
1170
|
+
// Place page files inside (app) route group if it exists
|
|
1171
|
+
// (but not API routes which stay under app/api/)
|
|
1172
|
+
if (hasAppGroup &&
|
|
1173
|
+
target.startsWith('app/') &&
|
|
1174
|
+
!target.startsWith('app/api/') &&
|
|
1175
|
+
!target.startsWith('app/dashboard/') &&
|
|
1176
|
+
target.endsWith('page.tsx')) {
|
|
1177
|
+
target = target.replace('app/', 'app/(app)/');
|
|
1178
|
+
}
|
|
1179
|
+
const targetPath = path_1.default.join(targetDir, target);
|
|
1180
|
+
// Ensure directory exists
|
|
1181
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(targetPath));
|
|
1182
|
+
// Generate content from inline template and write
|
|
1183
|
+
const content = file.generator();
|
|
1184
|
+
await fs_extra_1.default.writeFile(targetPath, content);
|
|
1185
|
+
spinner.text = `Adding ${config.displayName}... (${file.target})`;
|
|
1186
|
+
}
|
|
1187
|
+
// 1.5 Create required utility stubs if they don't already exist.
|
|
1188
|
+
// Some templates reference utilities (lib/audit.ts, lib/use-app-name.ts) that are
|
|
1189
|
+
// fully provided by admin-ui. When installing components standalone we create the
|
|
1190
|
+
// same full-featured files here so the project builds without admin-ui.
|
|
1191
|
+
if (config.requiredUtils) {
|
|
1192
|
+
for (const util of config.requiredUtils) {
|
|
1193
|
+
const utilPath = path_1.default.join(targetDir, util.target);
|
|
1194
|
+
if (!(await fs_extra_1.default.pathExists(utilPath))) {
|
|
1195
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(utilPath));
|
|
1196
|
+
await fs_extra_1.default.writeFile(utilPath, util.generator());
|
|
1197
|
+
spinner.text = `Adding ${config.displayName}... (created ${util.target})`;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
// 1.6 (removed) â We no longer patch the user's root layout.
|
|
1202
|
+
// Instead, the postAddMessage tells the user how to add SessionProvider.
|
|
1203
|
+
// 1.7 Auto-write environment variables to .env
|
|
1204
|
+
if (config.envVars && config.envVars.length > 0) {
|
|
1205
|
+
const envPath = path_1.default.join(targetDir, '.env');
|
|
1206
|
+
let envContent = '';
|
|
1207
|
+
if (await fs_extra_1.default.pathExists(envPath)) {
|
|
1208
|
+
envContent = await fs_extra_1.default.readFile(envPath, 'utf-8');
|
|
1209
|
+
}
|
|
1210
|
+
// Resolve __PORT_DETECT__ placeholder once (lazy)
|
|
1211
|
+
let resolvedPort = null;
|
|
1212
|
+
const newLines = [];
|
|
1213
|
+
for (const envVar of config.envVars) {
|
|
1214
|
+
if (!envContent.includes(`${envVar.key}=`)) {
|
|
1215
|
+
let val = typeof envVar.value === 'function' ? envVar.value() : envVar.value;
|
|
1216
|
+
// Replace port placeholder with detected port
|
|
1217
|
+
if (typeof val === 'string' && val.includes('__PORT_DETECT__')) {
|
|
1218
|
+
if (resolvedPort === null) {
|
|
1219
|
+
resolvedPort = await detectPort(targetDir);
|
|
1220
|
+
}
|
|
1221
|
+
val = val.replace('__PORT_DETECT__', String(resolvedPort));
|
|
1222
|
+
}
|
|
1223
|
+
if (envVar.comment) {
|
|
1224
|
+
newLines.push(`# ${envVar.comment}`);
|
|
1225
|
+
}
|
|
1226
|
+
newLines.push(`${envVar.key}=${val}`);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
if (newLines.length > 0) {
|
|
1230
|
+
const separator = envContent.length > 0 && !envContent.endsWith('\n') ? '\n' : '';
|
|
1231
|
+
await fs_extra_1.default.appendFile(envPath, `${separator}\n${newLines.join('\n')}\n`);
|
|
1232
|
+
spinner.text = `Adding ${config.displayName}... (configured .env)`;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
// 1.8 Generate seed scripts for runtime-readiness
|
|
1236
|
+
if (config.name === 'auth') {
|
|
1237
|
+
const seedPath = path_1.default.join(targetDir, 'prisma', 'seed.ts');
|
|
1238
|
+
if (!(await fs_extra_1.default.pathExists(seedPath))) {
|
|
1239
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(seedPath));
|
|
1240
|
+
await fs_extra_1.default.writeFile(seedPath, generateAuthSeed());
|
|
1241
|
+
spinner.text = `Adding ${config.displayName}... (created prisma/seed.ts)`;
|
|
1242
|
+
}
|
|
1243
|
+
// Wire prisma.seed in package.json (ts-node + esm)
|
|
1244
|
+
const pkgPath = path_1.default.join(targetDir, 'package.json');
|
|
1245
|
+
if (await fs_extra_1.default.pathExists(pkgPath)) {
|
|
1246
|
+
const pkg = await fs_extra_1.default.readJson(pkgPath);
|
|
1247
|
+
if (!pkg.prisma?.seed) {
|
|
1248
|
+
pkg.prisma = pkg.prisma ?? {};
|
|
1249
|
+
pkg.prisma.seed = 'npx tsx prisma/seed.ts';
|
|
1250
|
+
await fs_extra_1.default.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
1251
|
+
spinner.text = `Adding ${config.displayName}... (configured prisma.seed in package.json)`;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
if (config.name === 'model-providers') {
|
|
1256
|
+
const seedProvPath = path_1.default.join(targetDir, 'prisma', 'seed-providers.ts');
|
|
1257
|
+
if (!(await fs_extra_1.default.pathExists(seedProvPath))) {
|
|
1258
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(seedProvPath));
|
|
1259
|
+
await fs_extra_1.default.writeFile(seedProvPath, generateProviderSeed());
|
|
1260
|
+
spinner.text = `Adding ${config.displayName}... (created prisma/seed-providers.ts)`;
|
|
1261
|
+
}
|
|
1262
|
+
// Append provider-seed call to seed.ts if it exists (auth installed first)
|
|
1263
|
+
const seedPath = path_1.default.join(targetDir, 'prisma', 'seed.ts');
|
|
1264
|
+
if (await fs_extra_1.default.pathExists(seedPath)) {
|
|
1265
|
+
const seedContent = await fs_extra_1.default.readFile(seedPath, 'utf-8');
|
|
1266
|
+
if (!seedContent.includes('seed-providers')) {
|
|
1267
|
+
// Add import and call at the end of the main() function
|
|
1268
|
+
const patchedSeed = seedContent.replace(/main\(\)\s*\n\s*\.catch/, `main()\n .then(async () => {\n // Auto-added by chimerai add model-providers\n const { execSync } = require('child_process');\n execSync('npx tsx prisma/seed-providers.ts', { stdio: 'inherit', env: { ...process.env } });\n })\n .catch`);
|
|
1269
|
+
if (patchedSeed !== seedContent) {
|
|
1270
|
+
await fs_extra_1.default.writeFile(seedPath, patchedSeed);
|
|
1271
|
+
spinner.text = `Adding ${config.displayName}... (wired seed-providers into seed.ts)`;
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
// 1.9 Ensure Tailwind CSS is available (all templates use Tailwind utility classes)
|
|
1277
|
+
// Only runs once â checks if tailwindcss is already a dependency.
|
|
1278
|
+
// Installs Tailwind v4 (CSS-based config, no tailwind.config.js needed).
|
|
1279
|
+
{
|
|
1280
|
+
const packageJsonPath = path_1.default.join(targetDir, 'package.json');
|
|
1281
|
+
if (await fs_extra_1.default.pathExists(packageJsonPath)) {
|
|
1282
|
+
const pkg = await fs_extra_1.default.readJson(packageJsonPath);
|
|
1283
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1284
|
+
if (!allDeps['tailwindcss']) {
|
|
1285
|
+
spinner.text = `Adding ${config.displayName}... (setting up Tailwind CSS)`;
|
|
1286
|
+
// a) Add tailwindcss + postcss plugin as devDependencies
|
|
1287
|
+
pkg.devDependencies = pkg.devDependencies || {};
|
|
1288
|
+
pkg.devDependencies['tailwindcss'] = '^4';
|
|
1289
|
+
pkg.devDependencies['@tailwindcss/postcss'] = '^4';
|
|
1290
|
+
await fs_extra_1.default.writeJson(packageJsonPath, pkg, { spaces: 2 });
|
|
1291
|
+
// b) Create postcss.config.mjs if it doesn't exist
|
|
1292
|
+
const postcssConfigs = ['postcss.config.mjs', 'postcss.config.js', 'postcss.config.cjs'];
|
|
1293
|
+
const hasPostcss = (await Promise.all(postcssConfigs.map((f) => fs_extra_1.default.pathExists(path_1.default.join(targetDir, f))))).some(Boolean);
|
|
1294
|
+
if (!hasPostcss) {
|
|
1295
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'postcss.config.mjs'), `/** @type {import('postcss-load-config').Config} */\nconst config = {\n plugins: {\n "@tailwindcss/postcss": {},\n },\n};\n\nexport default config;\n`);
|
|
1296
|
+
}
|
|
1297
|
+
// c) Ensure Tailwind directives in globals.css (if not already present)
|
|
1298
|
+
// Wrap existing CSS in @layer base {} so stock Next.js resets
|
|
1299
|
+
// (e.g. * { padding:0; margin:0 }) don't override Tailwind
|
|
1300
|
+
// utility classes which live in @layer utilities.
|
|
1301
|
+
const globalsPath = path_1.default.join(targetDir, 'app', 'globals.css');
|
|
1302
|
+
if (await fs_extra_1.default.pathExists(globalsPath)) {
|
|
1303
|
+
const css = await fs_extra_1.default.readFile(globalsPath, 'utf-8');
|
|
1304
|
+
if (!css.includes('@import "tailwindcss"') &&
|
|
1305
|
+
!css.includes("@import 'tailwindcss'") &&
|
|
1306
|
+
!css.includes('@tailwind base')) {
|
|
1307
|
+
// Wrap existing CSS in @layer base so it doesn't conflict
|
|
1308
|
+
// with Tailwind's cascade layers (unlayered CSS beats
|
|
1309
|
+
// layered utilities regardless of specificity).
|
|
1310
|
+
const indented = css
|
|
1311
|
+
.split('\n')
|
|
1312
|
+
.map((line) => (line.trim() ? ` ${line}` : line))
|
|
1313
|
+
.join('\n');
|
|
1314
|
+
await fs_extra_1.default.writeFile(globalsPath, `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n@layer base {\n${indented}\n}\n`);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
else {
|
|
1318
|
+
// No globals.css â create one with Tailwind directives
|
|
1319
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'app'));
|
|
1320
|
+
await fs_extra_1.default.writeFile(path_1.default.join(targetDir, 'app', 'globals.css'), `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n`);
|
|
1321
|
+
}
|
|
1322
|
+
console.log(chalk_1.default.cyan(`\n đ¨ Tailwind CSS configured (all ChimerAI components use Tailwind)`));
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
// 2. Extend Prisma schema if needed
|
|
1327
|
+
if (config.schemaExtension) {
|
|
1328
|
+
const schemaPath = path_1.default.join(targetDir, 'prisma', 'schema.prisma');
|
|
1329
|
+
// Shared helper: inject a relation field before the closing brace of a Prisma model
|
|
1330
|
+
const injectRelation = (s, modelName, relation) => {
|
|
1331
|
+
// Check if this model already has the relation (whitespace-insensitive)
|
|
1332
|
+
const modelStart = s.indexOf(`model ${modelName} {`);
|
|
1333
|
+
if (modelStart === -1)
|
|
1334
|
+
return s;
|
|
1335
|
+
const nextModel = s.indexOf('\nmodel ', modelStart + 1);
|
|
1336
|
+
const modelBlock = nextModel === -1 ? s.substring(modelStart) : s.substring(modelStart, nextModel);
|
|
1337
|
+
// Extract the field name (first word) from the relation string
|
|
1338
|
+
// e.g. "apiUsage ApiUsage[]" â "apiUsage", "user User @relation(...)" â "user"
|
|
1339
|
+
const fieldName = relation.trim().split(/\s+/)[0];
|
|
1340
|
+
// Check if this field already exists in the model block (any spacing)
|
|
1341
|
+
const fieldRegex = new RegExp(`^\\s+${fieldName}\\s+`, 'm');
|
|
1342
|
+
if (fieldRegex.test(modelBlock))
|
|
1343
|
+
return s;
|
|
1344
|
+
// Find the closing brace line of this model (line-based, avoids regex issues with } in defaults)
|
|
1345
|
+
const lines = s.split('\n');
|
|
1346
|
+
let inModel = false;
|
|
1347
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1348
|
+
if (lines[i].includes(`model ${modelName} {`)) {
|
|
1349
|
+
inModel = true;
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1352
|
+
if (inModel && lines[i].trim() === '}') {
|
|
1353
|
+
lines.splice(i, 0, ` ${relation}`);
|
|
1354
|
+
return lines.join('\n');
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
return s;
|
|
1358
|
+
};
|
|
1359
|
+
// Registry of cross-component relations that need back-relations injected.
|
|
1360
|
+
// Runs after EVERY schema extension, regardless of which component triggered it.
|
|
1361
|
+
// This makes installation order irrelevant.
|
|
1362
|
+
// Each entry can have:
|
|
1363
|
+
// backRelation â injected into targetModel (the array/list side)
|
|
1364
|
+
// forwardRelation â injected into ifModel (the FK side, if missing)
|
|
1365
|
+
const CROSS_RELATIONS = [
|
|
1366
|
+
// chat-ui â auth: Conversation.user â User needs User.conversations
|
|
1367
|
+
{
|
|
1368
|
+
ifModel: 'Conversation',
|
|
1369
|
+
targetModel: 'User',
|
|
1370
|
+
backRelation: 'conversations Conversation[]',
|
|
1371
|
+
},
|
|
1372
|
+
// chat-ui â model-providers: Conversation.provider â Provider needs Provider.conversations
|
|
1373
|
+
{
|
|
1374
|
+
ifModel: 'Conversation',
|
|
1375
|
+
targetModel: 'Provider',
|
|
1376
|
+
backRelation: 'conversations Conversation[]',
|
|
1377
|
+
},
|
|
1378
|
+
// model-providers â auth: ApiUsage â User (both sides needed)
|
|
1379
|
+
{
|
|
1380
|
+
ifModel: 'ApiUsage',
|
|
1381
|
+
targetModel: 'User',
|
|
1382
|
+
backRelation: 'apiUsage ApiUsage[]',
|
|
1383
|
+
forwardRelation: 'user User @relation(fields: [userId], references: [id], onDelete: Cascade)',
|
|
1384
|
+
},
|
|
1385
|
+
];
|
|
1386
|
+
const fixCrossRelations = async () => {
|
|
1387
|
+
if (!(await fs_extra_1.default.pathExists(schemaPath)))
|
|
1388
|
+
return;
|
|
1389
|
+
let schema = await fs_extra_1.default.readFile(schemaPath, 'utf-8');
|
|
1390
|
+
let changed = false;
|
|
1391
|
+
for (const { ifModel, targetModel, backRelation, forwardRelation } of CROSS_RELATIONS) {
|
|
1392
|
+
if (schema.includes(`model ${ifModel} {`) && schema.includes(`model ${targetModel} {`)) {
|
|
1393
|
+
// Inject back-relation into targetModel (e.g. User.conversations)
|
|
1394
|
+
const afterBack = injectRelation(schema, targetModel, backRelation);
|
|
1395
|
+
if (afterBack !== schema) {
|
|
1396
|
+
schema = afterBack;
|
|
1397
|
+
changed = true;
|
|
1398
|
+
}
|
|
1399
|
+
// Inject forward-relation into ifModel if specified (e.g. ApiUsage.user)
|
|
1400
|
+
if (forwardRelation) {
|
|
1401
|
+
const afterFwd = injectRelation(schema, ifModel, forwardRelation);
|
|
1402
|
+
if (afterFwd !== schema) {
|
|
1403
|
+
schema = afterFwd;
|
|
1404
|
+
changed = true;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
if (changed) {
|
|
1410
|
+
await fs_extra_1.default.writeFile(schemaPath, schema);
|
|
1411
|
+
}
|
|
1412
|
+
};
|
|
1413
|
+
if (await fs_extra_1.default.pathExists(schemaPath)) {
|
|
1414
|
+
try {
|
|
1415
|
+
await (0, schema_manager_js_1.extendPrismaSchema)(schemaPath, config.schemaExtension);
|
|
1416
|
+
await fixCrossRelations();
|
|
1417
|
+
spinner.text = `Adding ${config.displayName}... (prisma schema extended)`;
|
|
1418
|
+
}
|
|
1419
|
+
catch (err) {
|
|
1420
|
+
console.log(chalk_1.default.yellow(`\n â ď¸ Could not extend Prisma schema: ${err.message}`));
|
|
1421
|
+
console.log(chalk_1.default.yellow(` You may need to add Conversation/Message models manually.`));
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
else {
|
|
1425
|
+
// === Schema does not exist â create base schema + apply extension ===
|
|
1426
|
+
console.log(chalk_1.default.yellow(`\n â ď¸ No Prisma schema found â creating base schema...`));
|
|
1427
|
+
// 1. Create prisma/ directory
|
|
1428
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'prisma'));
|
|
1429
|
+
// 2. Write base schema (generator + datasource)
|
|
1430
|
+
// Detect provider from DATABASE_URL in .env (default: sqlite for dev)
|
|
1431
|
+
const envPath = path_1.default.join(targetDir, '.env');
|
|
1432
|
+
let dbProvider = 'sqlite';
|
|
1433
|
+
if (await fs_extra_1.default.pathExists(envPath)) {
|
|
1434
|
+
const envContent = await fs_extra_1.default.readFile(envPath, 'utf-8');
|
|
1435
|
+
const dbUrlMatch = envContent.match(/DATABASE_URL\s*=\s*(.+)/);
|
|
1436
|
+
if (dbUrlMatch) {
|
|
1437
|
+
const dbUrl = dbUrlMatch[1].trim();
|
|
1438
|
+
if (dbUrl.startsWith('postgresql://') || dbUrl.startsWith('postgres://')) {
|
|
1439
|
+
dbProvider = 'postgresql';
|
|
1440
|
+
}
|
|
1441
|
+
else if (dbUrl.startsWith('mysql://')) {
|
|
1442
|
+
dbProvider = 'mysql';
|
|
1443
|
+
}
|
|
1444
|
+
// file: or anything else â sqlite
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
const baseSchema = `generator client {
|
|
1448
|
+
provider = "prisma-client-js"
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
datasource db {
|
|
1452
|
+
provider = "${dbProvider}"
|
|
1453
|
+
url = env("DATABASE_URL")
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
`;
|
|
1457
|
+
await fs_extra_1.default.writeFile(schemaPath, baseSchema);
|
|
1458
|
+
// 3. Apply schema extension
|
|
1459
|
+
try {
|
|
1460
|
+
await (0, schema_manager_js_1.extendPrismaSchema)(schemaPath, config.schemaExtension);
|
|
1461
|
+
await fixCrossRelations();
|
|
1462
|
+
spinner.text = `Adding ${config.displayName}... (prisma schema created + extended)`;
|
|
1463
|
+
}
|
|
1464
|
+
catch (err) {
|
|
1465
|
+
console.log(chalk_1.default.yellow(`\n â ď¸ Could not extend Prisma schema: ${err.message}`));
|
|
1466
|
+
console.log(chalk_1.default.yellow(` You may need to add the models manually.`));
|
|
1467
|
+
}
|
|
1468
|
+
// 4. Create lib/prisma.ts (PrismaClient singleton) if not present
|
|
1469
|
+
const prismaLibPath = path_1.default.join(targetDir, 'lib', 'prisma.ts');
|
|
1470
|
+
if (!(await fs_extra_1.default.pathExists(prismaLibPath))) {
|
|
1471
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(targetDir, 'lib'));
|
|
1472
|
+
await fs_extra_1.default.writeFile(prismaLibPath, templates.generatePrismaLib());
|
|
1473
|
+
}
|
|
1474
|
+
// 5. Add Prisma dependencies + scripts to package.json
|
|
1475
|
+
const packageJsonPath = path_1.default.join(targetDir, 'package.json');
|
|
1476
|
+
if (fs_extra_1.default.existsSync(packageJsonPath)) {
|
|
1477
|
+
const packageJson = await fs_extra_1.default.readJson(packageJsonPath);
|
|
1478
|
+
packageJson.dependencies = packageJson.dependencies || {};
|
|
1479
|
+
packageJson.devDependencies = packageJson.devDependencies || {};
|
|
1480
|
+
if (!packageJson.dependencies['@prisma/client']) {
|
|
1481
|
+
packageJson.dependencies['@prisma/client'] = '^5.22.0';
|
|
1482
|
+
}
|
|
1483
|
+
if (!packageJson.devDependencies['prisma']) {
|
|
1484
|
+
packageJson.devDependencies['prisma'] = '^5.22.0';
|
|
1485
|
+
}
|
|
1486
|
+
// Scripts
|
|
1487
|
+
packageJson.scripts = packageJson.scripts || {};
|
|
1488
|
+
if (!packageJson.scripts['db:push']) {
|
|
1489
|
+
packageJson.scripts['db:push'] = 'prisma db push';
|
|
1490
|
+
}
|
|
1491
|
+
if (!packageJson.scripts['db:generate']) {
|
|
1492
|
+
packageJson.scripts['db:generate'] = 'prisma generate';
|
|
1493
|
+
}
|
|
1494
|
+
if (!packageJson.scripts['db:studio']) {
|
|
1495
|
+
packageJson.scripts['db:studio'] = 'prisma studio';
|
|
1496
|
+
}
|
|
1497
|
+
await fs_extra_1.default.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
1498
|
+
}
|
|
1499
|
+
console.log(chalk_1.default.cyan(`\n đŚ Created Prisma base schema + dependencies`));
|
|
1500
|
+
console.log(chalk_1.default.yellow(` â Configure DATABASE_URL in .env (or run: chimerai setup database)`));
|
|
1501
|
+
// Optional: warn if referenced models are missing
|
|
1502
|
+
if (config.schemaExtension.includes('userId') &&
|
|
1503
|
+
!(await fs_extra_1.default.readFile(schemaPath, 'utf-8')).includes('model User {')) {
|
|
1504
|
+
console.log(chalk_1.default.yellow(` â ď¸ Schema references User model which doesn't exist yet.`));
|
|
1505
|
+
console.log(chalk_1.default.yellow(` Run: chimerai add auth`));
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
// 2.5 SQLite compatibility transform
|
|
1510
|
+
// Templates are PostgreSQL-native (production). When the datasource is
|
|
1511
|
+
// sqlite we post-process the schema to strip unsupported types.
|
|
1512
|
+
if (config.schemaExtension) {
|
|
1513
|
+
const schemaPath = path_1.default.join(targetDir, 'prisma', 'schema.prisma');
|
|
1514
|
+
if (await fs_extra_1.default.pathExists(schemaPath)) {
|
|
1515
|
+
const schema = await fs_extra_1.default.readFile(schemaPath, 'utf-8');
|
|
1516
|
+
if (schema.includes('provider = "sqlite"')) {
|
|
1517
|
+
const transformed = transformSchemaForSqlite(schema);
|
|
1518
|
+
if (transformed !== schema) {
|
|
1519
|
+
await fs_extra_1.default.writeFile(schemaPath, transformed);
|
|
1520
|
+
spinner.text = `Adding ${config.displayName}... (adapted schema for SQLite)`;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
// 3. Update package.json if dependencies required
|
|
1526
|
+
if (config.dependencies && Object.keys(config.dependencies).length > 0) {
|
|
1527
|
+
const packageJsonPath = path_1.default.join(targetDir, 'package.json');
|
|
1528
|
+
if (fs_extra_1.default.existsSync(packageJsonPath)) {
|
|
1529
|
+
const packageJson = await fs_extra_1.default.readJson(packageJsonPath);
|
|
1530
|
+
packageJson.dependencies = packageJson.dependencies || {};
|
|
1531
|
+
Object.entries(config.dependencies).forEach(([dep, version]) => {
|
|
1532
|
+
if (!packageJson.dependencies[dep]) {
|
|
1533
|
+
packageJson.dependencies[dep] = version;
|
|
1534
|
+
}
|
|
1535
|
+
});
|
|
1536
|
+
await fs_extra_1.default.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
// 3. Show success message
|
|
1540
|
+
spinner.succeed(chalk_1.default.green(config.postAddMessage));
|
|
1541
|
+
// 3.5 prompt-management: optionally patch chat/page.tsx if chat-ui is installed
|
|
1542
|
+
if (config.name === 'prompt-management') {
|
|
1543
|
+
// Cleanup legacy route-group prompts page to avoid duplicate /dashboard/prompts routes
|
|
1544
|
+
const legacyPromptPage = path_1.default.join(targetDir, 'app', '(app)', 'dashboard', 'prompts', 'page.tsx');
|
|
1545
|
+
const canonicalPromptPage = path_1.default.join(targetDir, 'app', 'dashboard', 'prompts', 'page.tsx');
|
|
1546
|
+
if ((await fs_extra_1.default.pathExists(legacyPromptPage)) && (await fs_extra_1.default.pathExists(canonicalPromptPage))) {
|
|
1547
|
+
await fs_extra_1.default.remove(legacyPromptPage);
|
|
1548
|
+
console.log(chalk_1.default.green(' â Removed legacy app/(app)/dashboard/prompts/page.tsx'));
|
|
1549
|
+
}
|
|
1550
|
+
// Patch dashboard sidebar link (/dashboard/prompts)
|
|
1551
|
+
const dashboardLayoutPath = path_1.default.join(targetDir, 'app', 'dashboard', 'layout.tsx');
|
|
1552
|
+
if (await fs_extra_1.default.pathExists(dashboardLayoutPath)) {
|
|
1553
|
+
let layout = await fs_extra_1.default.readFile(dashboardLayoutPath, 'utf-8');
|
|
1554
|
+
const original = layout;
|
|
1555
|
+
if (!layout.includes('/dashboard/prompts') && layout.includes('/dashboard/settings')) {
|
|
1556
|
+
layout = layout.replace(` <Link href="/dashboard/settings" onClick={() => setSidebarOpen(false)} className="block px-4 py-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 dark:text-gray-200">`, ` <Link href="/dashboard/prompts" onClick={() => setSidebarOpen(false)} className="block px-4 py-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 dark:text-gray-200">\n Prompt Templates\n </Link>\n <Link href="/dashboard/settings" onClick={() => setSidebarOpen(false)} className="block px-4 py-2 rounded hover:bg-gray-200 dark:hover:bg-gray-700 dark:text-gray-200">`);
|
|
1557
|
+
}
|
|
1558
|
+
if (layout !== original) {
|
|
1559
|
+
await fs_extra_1.default.writeFile(dashboardLayoutPath, layout, 'utf-8');
|
|
1560
|
+
console.log(chalk_1.default.green(' â Patched app/dashboard/layout.tsx â Prompt Templates link added'));
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
// Patch dashboard tiles page (/dashboard/prompts card)
|
|
1564
|
+
const dashboardPagePath = path_1.default.join(targetDir, 'app', 'dashboard', 'page.tsx');
|
|
1565
|
+
if (await fs_extra_1.default.pathExists(dashboardPagePath)) {
|
|
1566
|
+
let dashboardPage = await fs_extra_1.default.readFile(dashboardPagePath, 'utf-8');
|
|
1567
|
+
const original = dashboardPage;
|
|
1568
|
+
if (dashboardPage.includes(`const links = [`)) {
|
|
1569
|
+
dashboardPage = dashboardPage.replace(/\s*\{ href: '\/dashboard\/prompts', label: 'Prompt Templates', icon: 'đ', description: 'Create and manage system prompts' \},?/g, '');
|
|
1570
|
+
dashboardPage = dashboardPage.replace(` ];`, ` { href: '/dashboard/prompts', label: 'Prompt Templates', icon: 'đ', description: 'Create and manage system prompts' },\n ];`);
|
|
1571
|
+
}
|
|
1572
|
+
if (dashboardPage !== original) {
|
|
1573
|
+
await fs_extra_1.default.writeFile(dashboardPagePath, dashboardPage, 'utf-8');
|
|
1574
|
+
console.log(chalk_1.default.green(' â Patched app/dashboard/page.tsx â Prompt Templates tile added'));
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
const chatPageCandidates = [
|
|
1578
|
+
path_1.default.join(targetDir, 'app', '(app)', 'chat', 'page.tsx'),
|
|
1579
|
+
path_1.default.join(targetDir, 'app', 'chat', 'page.tsx'),
|
|
1580
|
+
];
|
|
1581
|
+
for (const chatPagePath of chatPageCandidates) {
|
|
1582
|
+
if (await fs_extra_1.default.pathExists(chatPagePath)) {
|
|
1583
|
+
let chatPage = await fs_extra_1.default.readFile(chatPagePath, 'utf-8');
|
|
1584
|
+
let patched = false;
|
|
1585
|
+
if (!chatPage.includes('PromptSelector') && chatPage.includes('ModelSelector')) {
|
|
1586
|
+
chatPage = chatPage.replace(`import { ModelSelector } from '@/components/chat/model-selector';`, `import { ModelSelector } from '@/components/chat/model-selector';\nimport { PromptSelector } from '@/components/chat/prompt-selector';`);
|
|
1587
|
+
patched = true;
|
|
1588
|
+
}
|
|
1589
|
+
if (!chatPage.includes('selectedPromptId') && chatPage.includes('setSelectedModelId,')) {
|
|
1590
|
+
chatPage = chatPage.replace(` setSelectedModelId,`, ` setSelectedModelId,\n selectedPromptId,\n setSelectedPromptId,`);
|
|
1591
|
+
patched = true;
|
|
1592
|
+
}
|
|
1593
|
+
if (!chatPage.includes('<PromptSelector') && chatPage.includes('<ModelSelector')) {
|
|
1594
|
+
chatPage = chatPage.replace(/(<ModelSelector[\s\S]*?disabled={isStreaming}\s*\/>)/, `$1\n <PromptSelector\n value={selectedPromptId}\n onChange={setSelectedPromptId}\n category="system"\n placeholder="No system prompt"\n />`);
|
|
1595
|
+
patched = true;
|
|
1596
|
+
}
|
|
1597
|
+
if (patched) {
|
|
1598
|
+
await fs_extra_1.default.writeFile(chatPagePath, chatPage, 'utf-8');
|
|
1599
|
+
console.log(chalk_1.default.green(` â Patched ${path_1.default.relative(targetDir, chatPagePath)} â PromptSelector added`));
|
|
1600
|
+
}
|
|
1601
|
+
break;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
const streamRoutePath = path_1.default.join(targetDir, 'app', 'api', 'v1', 'chat', 'stream', 'route.ts');
|
|
1605
|
+
if (await fs_extra_1.default.pathExists(streamRoutePath)) {
|
|
1606
|
+
let route = await fs_extra_1.default.readFile(streamRoutePath, 'utf-8');
|
|
1607
|
+
const original = route;
|
|
1608
|
+
// Ensure payload contains promptId
|
|
1609
|
+
route = route
|
|
1610
|
+
.replace(`const { messages, model, providerId, conversationId } = payload;`, `const { messages, model, providerId, conversationId, promptId } = payload;`)
|
|
1611
|
+
.replace(`const { messages, model, providerId, conversationId,promptId } = payload;`, `const { messages, model, providerId, conversationId, promptId } = payload;`);
|
|
1612
|
+
// Insert template-loading block only once
|
|
1613
|
+
if (!route.includes('let systemPromptContent: string | undefined;')) {
|
|
1614
|
+
route = route.replace(` // --- 3. Load provider and decrypt API key ---`, ` // --- 2.5 Load system prompt template ---\n let systemPromptContent: string | undefined;\n if (promptId) {\n const template = await (prisma as any).promptTemplate.findFirst({ where: { id: promptId, isActive: true } });\n if (template) systemPromptContent = template.content;\n }\n if (!systemPromptContent) {\n const def = await (prisma as any).promptTemplate.findFirst({ where: { category: 'system', isDefault: true, isActive: true } });\n if (def) systemPromptContent = def.content;\n }\n\n // --- 3. Load provider and decrypt API key ---`);
|
|
1615
|
+
}
|
|
1616
|
+
// Anthropic: dedicated system field
|
|
1617
|
+
route = route.replace(`system: messages.find((m: any) => m.role === 'system')?.content || undefined,`, `system: systemPromptContent || messages.find((m: any) => m.role === 'system')?.content || undefined,`);
|
|
1618
|
+
// Ollama/OpenAI-like providers: prepend system message if selected/default template exists
|
|
1619
|
+
const promptMessagesBlock = `messages: systemPromptContent\n ? [{ role: 'system', content: systemPromptContent }, ...messages.filter((m: any) => m.role !== 'system')]\n : messages,`;
|
|
1620
|
+
// Replace plain `messages` payload in both blocks (ollama + openai-like)
|
|
1621
|
+
route = route.replace(` model: modelId,\n messages,\n stream: true,`, ` model: modelId,\n ${promptMessagesBlock}\n stream: true,`);
|
|
1622
|
+
route = route.replace(` model: modelId,\n messages,\n stream: true,`, ` model: modelId,\n ${promptMessagesBlock}\n stream: true,`);
|
|
1623
|
+
// Heal accidental duplicated ternary from previous patch versions
|
|
1624
|
+
route = route.replace(` messages: systemPromptContent\n ? [{ role: 'system', content: systemPromptContent }, ...messages.filter((m: any) => m.role !== 'system')]\n : messages: systemPromptContent\n ? [{ role: 'system', content: systemPromptContent }, ...messages.filter((m: any) => m.role !== 'system')]\n : messages,`, ` ${promptMessagesBlock}`);
|
|
1625
|
+
if (route !== original) {
|
|
1626
|
+
await fs_extra_1.default.writeFile(streamRoutePath, route, 'utf-8');
|
|
1627
|
+
console.log(chalk_1.default.green(` â Patched app/api/v1/chat/stream/route.ts â promptId support added`));
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
const useChatPath = path_1.default.join(targetDir, 'components', 'chat', 'use-chat.ts');
|
|
1631
|
+
if (await fs_extra_1.default.pathExists(useChatPath)) {
|
|
1632
|
+
let useChat = await fs_extra_1.default.readFile(useChatPath, 'utf-8');
|
|
1633
|
+
if (!useChat.includes('selectedPromptId') && useChat.includes('setSystemPrompt')) {
|
|
1634
|
+
useChat = useChat
|
|
1635
|
+
.replace(` setSystemPrompt: (prompt: string) => void;`, ` setSystemPrompt: (prompt: string) => void;\n selectedPromptId: string | null;\n setSelectedPromptId: (id: string | null) => void;`)
|
|
1636
|
+
.replace(` const [systemPrompt, setSystemPrompt] = useState('');`, ` const [systemPrompt, setSystemPrompt] = useState('');\n const [selectedPromptId, setSelectedPromptId] = useState<string | null>(null);`)
|
|
1637
|
+
.replace(` conversationId: conversationIdRef.current || undefined,\n providerId,\n };`, ` conversationId: conversationIdRef.current || undefined,\n providerId,\n ...(selectedPromptId ? { promptId: selectedPromptId } : {}),\n };`)
|
|
1638
|
+
.replace(` setSystemPrompt,\n regenerateMessage,`, ` setSystemPrompt,\n selectedPromptId,\n setSelectedPromptId,\n regenerateMessage,`)
|
|
1639
|
+
.replace(` [isStreaming, messages, models, selectedModelId, systemPrompt, options, refreshConversations, fetchCreditBalance]`, ` [isStreaming, messages, models, selectedModelId, selectedPromptId, systemPrompt, options, refreshConversations, fetchCreditBalance]`);
|
|
1640
|
+
await fs_extra_1.default.writeFile(useChatPath, useChat, 'utf-8');
|
|
1641
|
+
console.log(chalk_1.default.green(` â Patched components/chat/use-chat.ts â selectedPromptId added`));
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
// 4. Show what was added
|
|
1646
|
+
console.log(chalk_1.default.cyan('\n Files added:'));
|
|
1647
|
+
config.files.forEach((file) => {
|
|
1648
|
+
console.log(chalk_1.default.cyan(` â ${file.target}`));
|
|
1649
|
+
});
|
|
1650
|
+
if (config.dependencies && Object.keys(config.dependencies).length > 0) {
|
|
1651
|
+
console.log(chalk_1.default.cyan('\n Installing dependencies...'));
|
|
1652
|
+
try {
|
|
1653
|
+
(0, child_process_1.execSync)('pnpm install', { cwd: targetDir, stdio: 'inherit' });
|
|
1654
|
+
console.log(chalk_1.default.green(' â Dependencies installed'));
|
|
1655
|
+
}
|
|
1656
|
+
catch {
|
|
1657
|
+
console.log(chalk_1.default.yellow('\n Dependencies to install:'));
|
|
1658
|
+
Object.entries(config.dependencies).forEach(([dep, version]) => {
|
|
1659
|
+
console.log(chalk_1.default.yellow(` ⢠${dep}@${version}`));
|
|
1660
|
+
});
|
|
1661
|
+
console.log(chalk_1.default.white(' Run: pnpm install'));
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
catch (error) {
|
|
1666
|
+
spinner.fail(chalk_1.default.red(`Failed to add ${config.displayName}`));
|
|
1667
|
+
console.error(error.message);
|
|
1668
|
+
throw error;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
// ============================================================================
|
|
1672
|
+
// AI Service â Modular installation logic
|
|
1673
|
+
// ============================================================================
|
|
1674
|
+
const AI_SERVICE_DIR = 'services/ai';
|
|
1675
|
+
/**
|
|
1676
|
+
* Ensure the core AI service structure exists.
|
|
1677
|
+
* Creates services/ai/ with config.py, provider_client.py, services/__init__.py, routes/__init__.py
|
|
1678
|
+
*/
|
|
1679
|
+
async function ensureAiServiceCore(targetDir, spinner) {
|
|
1680
|
+
const aiDir = path_1.default.join(targetDir, AI_SERVICE_DIR);
|
|
1681
|
+
const manifestPath = path_1.default.join(aiDir, '.chimerai-ai');
|
|
1682
|
+
// If manifest exists, read and return it
|
|
1683
|
+
if (await fs_extra_1.default.pathExists(manifestPath)) {
|
|
1684
|
+
return templates.readAiManifest(targetDir);
|
|
1685
|
+
}
|
|
1686
|
+
// First time â create core structure
|
|
1687
|
+
spinner.text = 'Creating AI service core structure...';
|
|
1688
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(aiDir, 'services'));
|
|
1689
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(aiDir, 'routes'));
|
|
1690
|
+
// Core files (always present)
|
|
1691
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'config.py'), templates.generateAiServiceConfig([]));
|
|
1692
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'provider_client.py'), templates.generateProviderClient());
|
|
1693
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'services', '__init__.py'), templates.generateServicesInit());
|
|
1694
|
+
// Initial manifest
|
|
1695
|
+
const manifest = {
|
|
1696
|
+
modules: [],
|
|
1697
|
+
tools: [],
|
|
1698
|
+
installedAt: new Date().toISOString().split('T')[0],
|
|
1699
|
+
cliVersion: '1.0.0',
|
|
1700
|
+
};
|
|
1701
|
+
templates.writeAiManifest(targetDir, manifest);
|
|
1702
|
+
return manifest;
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Regenerate dynamic files (main.py, models.py, requirements.txt, README.md, routes/__init__.py)
|
|
1706
|
+
* based on the current manifest state.
|
|
1707
|
+
*/
|
|
1708
|
+
async function regenerateAiServiceFiles(targetDir, manifest, spinner) {
|
|
1709
|
+
const aiDir = path_1.default.join(targetDir, AI_SERVICE_DIR);
|
|
1710
|
+
spinner.text = 'Regenerating AI service files...';
|
|
1711
|
+
// Dynamic files
|
|
1712
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'main.py'), templates.generateAiServiceMain(manifest.modules, manifest.tools));
|
|
1713
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'models.py'), templates.generateAiServiceModels(manifest.modules));
|
|
1714
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'requirements.txt'), templates.generateAiServiceRequirements(manifest.modules, manifest.tools));
|
|
1715
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'config.py'), templates.generateAiServiceConfig(manifest.modules));
|
|
1716
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'README.md'), templates.generateAiServiceReadme(manifest.modules, manifest.tools));
|
|
1717
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'routes', '__init__.py'), templates.generateRoutesInit());
|
|
1718
|
+
// Dockerfile (static but only created once)
|
|
1719
|
+
const dockerfilePath = path_1.default.join(aiDir, 'Dockerfile');
|
|
1720
|
+
if (!(await fs_extra_1.default.pathExists(dockerfilePath))) {
|
|
1721
|
+
await fs_extra_1.default.writeFile(dockerfilePath, templates.generateAiServiceDockerfile());
|
|
1722
|
+
}
|
|
1723
|
+
// Update manifest
|
|
1724
|
+
templates.writeAiManifest(targetDir, manifest);
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Extend docker-compose.yml with ai-service block if not already present
|
|
1728
|
+
*/
|
|
1729
|
+
async function ensureDockerComposeAiService(targetDir) {
|
|
1730
|
+
const composePath = path_1.default.join(targetDir, 'docker-compose.yml');
|
|
1731
|
+
if (!(await fs_extra_1.default.pathExists(composePath)))
|
|
1732
|
+
return;
|
|
1733
|
+
const content = await fs_extra_1.default.readFile(composePath, 'utf-8');
|
|
1734
|
+
if (content.includes('ai-service:'))
|
|
1735
|
+
return; // Already has AI service block
|
|
1736
|
+
// Append AI service block before the last line (networks/volumes)
|
|
1737
|
+
const aiBlock = templates.generateDockerComposeAiService();
|
|
1738
|
+
// Simple approach: append the service block to the services section
|
|
1739
|
+
const updatedContent = content.replace(/^(services:.*)/m, `$1\n${aiBlock}`);
|
|
1740
|
+
// If that didn't work (no 'services:' header found), append at end
|
|
1741
|
+
if (updatedContent === content) {
|
|
1742
|
+
await fs_extra_1.default.appendFile(composePath, '\n' + aiBlock);
|
|
1743
|
+
}
|
|
1744
|
+
else {
|
|
1745
|
+
await fs_extra_1.default.writeFile(composePath, updatedContent);
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
/**
|
|
1749
|
+
* Ensure .env has AI_SERVICE_URL
|
|
1750
|
+
*/
|
|
1751
|
+
async function ensureEnvAiServiceUrl(targetDir) {
|
|
1752
|
+
const envPath = path_1.default.join(targetDir, '.env');
|
|
1753
|
+
const envLine = 'AI_SERVICE_URL=http://localhost:8002';
|
|
1754
|
+
if (await fs_extra_1.default.pathExists(envPath)) {
|
|
1755
|
+
const content = await fs_extra_1.default.readFile(envPath, 'utf-8');
|
|
1756
|
+
if (content.includes('AI_SERVICE_URL'))
|
|
1757
|
+
return;
|
|
1758
|
+
await fs_extra_1.default.appendFile(envPath, `\n# AI Service\n${envLine}\n`);
|
|
1759
|
+
}
|
|
1760
|
+
else {
|
|
1761
|
+
await fs_extra_1.default.writeFile(envPath, `# AI Service\n${envLine}\n`);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Ensure .gitignore has AI service entries
|
|
1766
|
+
*/
|
|
1767
|
+
async function ensureGitignoreAiEntries(targetDir) {
|
|
1768
|
+
const gitignorePath = path_1.default.join(targetDir, '.gitignore');
|
|
1769
|
+
const entries = ['services/ai/.venv/', 'services/ai/data/*', '!services/ai/data/.gitkeep'];
|
|
1770
|
+
if (await fs_extra_1.default.pathExists(gitignorePath)) {
|
|
1771
|
+
let content = await fs_extra_1.default.readFile(gitignorePath, 'utf-8');
|
|
1772
|
+
const missing = entries.filter((e) => !content.includes(e));
|
|
1773
|
+
if (missing.length > 0) {
|
|
1774
|
+
content += `\n# AI Service\n${missing.join('\n')}\n`;
|
|
1775
|
+
await fs_extra_1.default.writeFile(gitignorePath, content);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
// ---- Individual AI module installers ----
|
|
1780
|
+
async function addAiChat(targetDir) {
|
|
1781
|
+
const spinner = (0, ora_1.default)('Adding AI Chat module...').start();
|
|
1782
|
+
try {
|
|
1783
|
+
const manifest = await ensureAiServiceCore(targetDir, spinner);
|
|
1784
|
+
const aiDir = path_1.default.join(targetDir, AI_SERVICE_DIR);
|
|
1785
|
+
if (manifest.modules.includes('chat')) {
|
|
1786
|
+
spinner.info(chalk_1.default.yellow('AI Chat module is already installed.'));
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
// Write chat-specific service files
|
|
1790
|
+
spinner.text = 'Installing chat services...';
|
|
1791
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'services', 'chat_service.py'), templates.generateChatService());
|
|
1792
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'services', 'model_service.py'), templates.generateModelService());
|
|
1793
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'services', 'moderation_service.py'), templates.generateModerationService());
|
|
1794
|
+
// Write chat routes
|
|
1795
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(aiDir, 'routes'));
|
|
1796
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'routes', 'chat_routes.py'), templates.generateChatRoutes());
|
|
1797
|
+
// Update manifest
|
|
1798
|
+
manifest.modules.push('chat');
|
|
1799
|
+
// Regenerate dynamic files
|
|
1800
|
+
await regenerateAiServiceFiles(targetDir, manifest, spinner);
|
|
1801
|
+
// Docker + env
|
|
1802
|
+
await ensureDockerComposeAiService(targetDir);
|
|
1803
|
+
await ensureEnvAiServiceUrl(targetDir);
|
|
1804
|
+
await ensureGitignoreAiEntries(targetDir);
|
|
1805
|
+
spinner.succeed(chalk_1.default.green('AI Chat module installed'));
|
|
1806
|
+
console.log(chalk_1.default.cyan('\n Files added:'));
|
|
1807
|
+
console.log(chalk_1.default.cyan(' â services/ai/config.py'));
|
|
1808
|
+
console.log(chalk_1.default.cyan(' â services/ai/provider_client.py'));
|
|
1809
|
+
console.log(chalk_1.default.cyan(' â services/ai/services/chat_service.py'));
|
|
1810
|
+
console.log(chalk_1.default.cyan(' â services/ai/services/model_service.py'));
|
|
1811
|
+
console.log(chalk_1.default.cyan(' â services/ai/services/moderation_service.py'));
|
|
1812
|
+
console.log(chalk_1.default.cyan(' â services/ai/routes/chat_routes.py'));
|
|
1813
|
+
console.log(chalk_1.default.cyan(' â services/ai/main.py (generated)'));
|
|
1814
|
+
console.log(chalk_1.default.cyan(' â services/ai/models.py (generated)'));
|
|
1815
|
+
console.log(chalk_1.default.cyan(' â services/ai/requirements.txt (generated)'));
|
|
1816
|
+
console.log(chalk_1.default.white('\n To start the AI service:'));
|
|
1817
|
+
console.log(chalk_1.default.gray(' cd services/ai'));
|
|
1818
|
+
console.log(chalk_1.default.gray(' python -m venv .venv'));
|
|
1819
|
+
console.log(chalk_1.default.gray(' .venv/Scripts/activate (Windows) or source .venv/bin/activate'));
|
|
1820
|
+
console.log(chalk_1.default.gray(' pip install -r requirements.txt'));
|
|
1821
|
+
console.log(chalk_1.default.gray(' uvicorn main:app --reload --port 8002'));
|
|
1822
|
+
console.log(chalk_1.default.white('\n Or use: chimerai dev (starts both Next.js + AI service)'));
|
|
1823
|
+
}
|
|
1824
|
+
catch (error) {
|
|
1825
|
+
spinner.fail(chalk_1.default.red('Failed to add AI Chat module'));
|
|
1826
|
+
console.error(error.message);
|
|
1827
|
+
throw error;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
async function addRag(targetDir) {
|
|
1831
|
+
const spinner = (0, ora_1.default)('Adding RAG module...').start();
|
|
1832
|
+
try {
|
|
1833
|
+
const manifest = await ensureAiServiceCore(targetDir, spinner);
|
|
1834
|
+
const aiDir = path_1.default.join(targetDir, AI_SERVICE_DIR);
|
|
1835
|
+
if (manifest.modules.includes('rag')) {
|
|
1836
|
+
spinner.info(chalk_1.default.yellow('RAG module is already installed.'));
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1839
|
+
// RAG requires chat â auto-install if missing
|
|
1840
|
+
if (!manifest.modules.includes('chat')) {
|
|
1841
|
+
spinner.info(chalk_1.default.cyan('âšď¸ RAG requires the Chat module. Installing both...'));
|
|
1842
|
+
spinner.start('Installing Chat module first...');
|
|
1843
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'services', 'chat_service.py'), templates.generateChatService());
|
|
1844
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'services', 'model_service.py'), templates.generateModelService());
|
|
1845
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'services', 'moderation_service.py'), templates.generateModerationService());
|
|
1846
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(aiDir, 'routes'));
|
|
1847
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'routes', 'chat_routes.py'), templates.generateChatRoutes());
|
|
1848
|
+
manifest.modules.push('chat');
|
|
1849
|
+
}
|
|
1850
|
+
// Write RAG-specific service files
|
|
1851
|
+
spinner.text = 'Installing RAG services...';
|
|
1852
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'services', 'rag_service.py'), templates.generateRagService());
|
|
1853
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'services', 'vector_store.py'), templates.generateVectorStore());
|
|
1854
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'services', 'embedding_service.py'), templates.generateEmbeddingService());
|
|
1855
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'services', 'file_extractor.py'), templates.generateFileExtractor());
|
|
1856
|
+
// RAG routes
|
|
1857
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'routes', 'rag_routes.py'), templates.generateRagRoutes());
|
|
1858
|
+
// Create data directory with .gitkeep
|
|
1859
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(aiDir, 'data'));
|
|
1860
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'data', '.gitkeep'), '');
|
|
1861
|
+
// Update manifest
|
|
1862
|
+
manifest.modules.push('rag');
|
|
1863
|
+
// Regenerate dynamic files
|
|
1864
|
+
await regenerateAiServiceFiles(targetDir, manifest, spinner);
|
|
1865
|
+
// Docker + env
|
|
1866
|
+
await ensureDockerComposeAiService(targetDir);
|
|
1867
|
+
await ensureEnvAiServiceUrl(targetDir);
|
|
1868
|
+
await ensureGitignoreAiEntries(targetDir);
|
|
1869
|
+
// ââ Frontend proxy routes + RAG page ââââââââââââââââââââââââââââââââââ
|
|
1870
|
+
spinner.start('Installing RAG frontend routes...');
|
|
1871
|
+
const srcDir = path_1.default.join(targetDir, 'src');
|
|
1872
|
+
const appBase = (await fs_extra_1.default.pathExists(srcDir)) ? srcDir : targetDir;
|
|
1873
|
+
// lib/rag.ts â AI Service client helper
|
|
1874
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(appBase, 'lib'));
|
|
1875
|
+
await fs_extra_1.default.writeFile(path_1.default.join(appBase, 'lib', 'rag.ts'), templates.generateRagLib());
|
|
1876
|
+
// API proxy routes
|
|
1877
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(appBase, 'app', 'api', 'rag', 'query'));
|
|
1878
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(appBase, 'app', 'api', 'rag', 'stats'));
|
|
1879
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(appBase, 'app', 'api', 'rag', 'clear'));
|
|
1880
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(appBase, 'app', 'api', 'rag', 'delete'));
|
|
1881
|
+
await fs_extra_1.default.writeFile(path_1.default.join(appBase, 'app', 'api', 'rag', 'route.ts'), templates.generateRagUploadRoute());
|
|
1882
|
+
await fs_extra_1.default.writeFile(path_1.default.join(appBase, 'app', 'api', 'rag', 'query', 'route.ts'), templates.generateRagQueryRoute());
|
|
1883
|
+
await fs_extra_1.default.writeFile(path_1.default.join(appBase, 'app', 'api', 'rag', 'stats', 'route.ts'), templates.generateRagStatsRoute());
|
|
1884
|
+
await fs_extra_1.default.writeFile(path_1.default.join(appBase, 'app', 'api', 'rag', 'clear', 'route.ts'), templates.generateRagClearRoute());
|
|
1885
|
+
await fs_extra_1.default.writeFile(path_1.default.join(appBase, 'app', 'api', 'rag', 'delete', 'route.ts'), templates.generateRagDeleteRoute());
|
|
1886
|
+
// RAG page
|
|
1887
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(appBase, 'app', 'rag'));
|
|
1888
|
+
await fs_extra_1.default.writeFile(path_1.default.join(appBase, 'app', 'rag', 'page.tsx'), templates.generateRagPage());
|
|
1889
|
+
spinner.succeed(chalk_1.default.green('RAG module installed'));
|
|
1890
|
+
console.log(chalk_1.default.cyan('\n Files added (AI Service):'));
|
|
1891
|
+
console.log(chalk_1.default.cyan(' â services/ai/services/rag_service.py'));
|
|
1892
|
+
console.log(chalk_1.default.cyan(' â services/ai/services/vector_store.py'));
|
|
1893
|
+
console.log(chalk_1.default.cyan(' â services/ai/services/embedding_service.py'));
|
|
1894
|
+
console.log(chalk_1.default.cyan(' â services/ai/services/file_extractor.py'));
|
|
1895
|
+
console.log(chalk_1.default.cyan(' â services/ai/routes/rag_routes.py'));
|
|
1896
|
+
console.log(chalk_1.default.cyan(' â services/ai/data/.gitkeep'));
|
|
1897
|
+
console.log(chalk_1.default.cyan('\n Files added (Frontend):'));
|
|
1898
|
+
console.log(chalk_1.default.cyan(' â lib/rag.ts'));
|
|
1899
|
+
console.log(chalk_1.default.cyan(' â app/api/rag/route.ts (upload)'));
|
|
1900
|
+
console.log(chalk_1.default.cyan(' â app/api/rag/query/route.ts'));
|
|
1901
|
+
console.log(chalk_1.default.cyan(' â app/api/rag/stats/route.ts'));
|
|
1902
|
+
console.log(chalk_1.default.cyan(' â app/api/rag/clear/route.ts'));
|
|
1903
|
+
console.log(chalk_1.default.cyan(' â app/api/rag/delete/route.ts'));
|
|
1904
|
+
console.log(chalk_1.default.cyan(' â app/rag/page.tsx'));
|
|
1905
|
+
if (!manifest.modules.includes('chat')) {
|
|
1906
|
+
console.log(chalk_1.default.cyan(' â (Chat module auto-installed)'));
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
catch (error) {
|
|
1910
|
+
spinner.fail(chalk_1.default.red('Failed to add RAG module'));
|
|
1911
|
+
console.error(error.message);
|
|
1912
|
+
throw error;
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
async function addGuardrails(targetDir) {
|
|
1916
|
+
(0, license_js_1.assertComponentAccess)('guardrails');
|
|
1917
|
+
const spinner = (0, ora_1.default)('Adding Guardrails module...').start();
|
|
1918
|
+
try {
|
|
1919
|
+
const manifest = await ensureAiServiceCore(targetDir, spinner);
|
|
1920
|
+
const aiDir = path_1.default.join(targetDir, AI_SERVICE_DIR);
|
|
1921
|
+
if (manifest.modules.includes('guardrails')) {
|
|
1922
|
+
spinner.info(chalk_1.default.yellow('Guardrails module is already installed.'));
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
// Write guardrails service
|
|
1926
|
+
spinner.text = 'Installing guardrails service...';
|
|
1927
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'services', 'guardrails_service.py'), templates.generateGuardrailsService());
|
|
1928
|
+
// Guardrails routes
|
|
1929
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(aiDir, 'routes'));
|
|
1930
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'routes', 'guardrails_routes.py'), templates.generateGuardrailsRoutes());
|
|
1931
|
+
// Update manifest
|
|
1932
|
+
manifest.modules.push('guardrails');
|
|
1933
|
+
// Regenerate dynamic files
|
|
1934
|
+
await regenerateAiServiceFiles(targetDir, manifest, spinner);
|
|
1935
|
+
// Docker + env
|
|
1936
|
+
await ensureDockerComposeAiService(targetDir);
|
|
1937
|
+
await ensureEnvAiServiceUrl(targetDir);
|
|
1938
|
+
await ensureGitignoreAiEntries(targetDir);
|
|
1939
|
+
spinner.succeed(chalk_1.default.green('Guardrails module installed'));
|
|
1940
|
+
console.log(chalk_1.default.cyan('\n Files added:'));
|
|
1941
|
+
console.log(chalk_1.default.cyan(' â services/ai/services/guardrails_service.py'));
|
|
1942
|
+
console.log(chalk_1.default.cyan(' â services/ai/routes/guardrails_routes.py'));
|
|
1943
|
+
}
|
|
1944
|
+
catch (error) {
|
|
1945
|
+
spinner.fail(chalk_1.default.red('Failed to add Guardrails module'));
|
|
1946
|
+
console.error(error.message);
|
|
1947
|
+
throw error;
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
async function addAiTools(targetDir) {
|
|
1951
|
+
(0, license_js_1.assertComponentAccess)('ai-tools');
|
|
1952
|
+
const spinner = (0, ora_1.default)('Preparing AI Tools...').start();
|
|
1953
|
+
try {
|
|
1954
|
+
const manifest = await ensureAiServiceCore(targetDir, spinner);
|
|
1955
|
+
const aiDir = path_1.default.join(targetDir, AI_SERVICE_DIR);
|
|
1956
|
+
spinner.stop();
|
|
1957
|
+
// Build choices for interactive menu
|
|
1958
|
+
const toolChoices = Object.entries(templates.TOOL_INFO).map(([key, info]) => ({
|
|
1959
|
+
name: `${info.displayName} â ${info.description}`,
|
|
1960
|
+
value: key,
|
|
1961
|
+
checked: manifest.tools.includes(key), // Pre-check already installed tools
|
|
1962
|
+
}));
|
|
1963
|
+
const { selectedTools } = await inquirer_1.default.prompt([
|
|
1964
|
+
{
|
|
1965
|
+
type: 'checkbox',
|
|
1966
|
+
name: 'selectedTools',
|
|
1967
|
+
message: 'Select AI tools to install:',
|
|
1968
|
+
choices: toolChoices,
|
|
1969
|
+
pageSize: 12,
|
|
1970
|
+
},
|
|
1971
|
+
]);
|
|
1972
|
+
if (selectedTools.length === 0) {
|
|
1973
|
+
console.log(chalk_1.default.yellow('\n No tools selected. Skipping.\n'));
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
spinner.start('Installing AI tools...');
|
|
1977
|
+
// Create tools directory
|
|
1978
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(aiDir, 'services', 'tools'));
|
|
1979
|
+
// Install each selected tool
|
|
1980
|
+
const newTools = [];
|
|
1981
|
+
for (const toolKey of selectedTools) {
|
|
1982
|
+
if (manifest.tools.includes(toolKey))
|
|
1983
|
+
continue; // Already installed
|
|
1984
|
+
const generator = ai_service_tools_js_1.TOOL_GENERATORS[toolKey];
|
|
1985
|
+
if (!generator)
|
|
1986
|
+
continue;
|
|
1987
|
+
spinner.text = `Installing ${templates.TOOL_INFO[toolKey]?.displayName || toolKey}...`;
|
|
1988
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'services', 'tools', `${toolKey}.py`), generator());
|
|
1989
|
+
newTools.push(toolKey);
|
|
1990
|
+
}
|
|
1991
|
+
if (newTools.length === 0) {
|
|
1992
|
+
spinner.info(chalk_1.default.yellow('All selected tools are already installed.'));
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
// Update manifest
|
|
1996
|
+
manifest.tools = [...new Set([...manifest.tools, ...selectedTools])];
|
|
1997
|
+
if (!manifest.modules.includes('tools')) {
|
|
1998
|
+
manifest.modules.push('tools');
|
|
1999
|
+
}
|
|
2000
|
+
// Generate tools __init__.py
|
|
2001
|
+
const { generateToolsInit } = await import('../templates/ai-service-tools.js');
|
|
2002
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'services', 'tools', '__init__.py'), generateToolsInit(manifest.tools));
|
|
2003
|
+
// Generate tools routes
|
|
2004
|
+
await fs_extra_1.default.ensureDir(path_1.default.join(aiDir, 'routes'));
|
|
2005
|
+
await fs_extra_1.default.writeFile(path_1.default.join(aiDir, 'routes', 'tools_routes.py'), templates.generateToolsRoutes(manifest.tools));
|
|
2006
|
+
// Regenerate dynamic files
|
|
2007
|
+
await regenerateAiServiceFiles(targetDir, manifest, spinner);
|
|
2008
|
+
// Docker + env
|
|
2009
|
+
await ensureDockerComposeAiService(targetDir);
|
|
2010
|
+
await ensureEnvAiServiceUrl(targetDir);
|
|
2011
|
+
await ensureGitignoreAiEntries(targetDir);
|
|
2012
|
+
spinner.succeed(chalk_1.default.green(`${newTools.length} AI tool(s) installed`));
|
|
2013
|
+
console.log(chalk_1.default.cyan('\n Tools installed:'));
|
|
2014
|
+
for (const toolKey of newTools) {
|
|
2015
|
+
const info = templates.TOOL_INFO[toolKey];
|
|
2016
|
+
console.log(chalk_1.default.cyan(` â ${info?.displayName || toolKey}`));
|
|
2017
|
+
}
|
|
2018
|
+
// Show required env vars for specific tools
|
|
2019
|
+
const needsGoogleCreds = newTools.includes('google_sheets_tools');
|
|
2020
|
+
const needsAirtable = newTools.includes('airtable_tools');
|
|
2021
|
+
const needsDeepl = newTools.includes('deepl_tools');
|
|
2022
|
+
if (needsGoogleCreds || needsAirtable || needsDeepl) {
|
|
2023
|
+
console.log(chalk_1.default.yellow('\n Required environment variables:'));
|
|
2024
|
+
if (needsGoogleCreds)
|
|
2025
|
+
console.log(chalk_1.default.yellow(' ⢠GOOGLE_SERVICE_ACCOUNT_JSON=path/to/credentials.json'));
|
|
2026
|
+
if (needsAirtable)
|
|
2027
|
+
console.log(chalk_1.default.yellow(' ⢠AIRTABLE_API_KEY=your-api-key'));
|
|
2028
|
+
if (needsDeepl)
|
|
2029
|
+
console.log(chalk_1.default.yellow(' ⢠DEEPL_AUTH_KEY=your-auth-key'));
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
catch (error) {
|
|
2033
|
+
spinner.fail(chalk_1.default.red('Failed to add AI tools'));
|
|
2034
|
+
console.error(error.message);
|
|
2035
|
+
throw error;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
async function addCommand(component, options) {
|
|
2039
|
+
console.log(chalk_1.default.bold.cyan(`\nđŚ Adding ${component} to your project\n`));
|
|
2040
|
+
// đ Edition gate â exits with upsell message if Pro component and no license
|
|
2041
|
+
(0, license_js_1.assertComponentAccess)(component);
|
|
2042
|
+
const targetDir = (0, utils_js_1.resolveTargetDir)(options.dir);
|
|
2043
|
+
console.log(chalk_1.default.gray(` Project: ${targetDir}\n`));
|
|
2044
|
+
// Ensure the target is marked as a ChimerAI project
|
|
2045
|
+
const markerPath = path_1.default.join(targetDir, '.chimerai');
|
|
2046
|
+
if (!fs_extra_1.default.existsSync(markerPath) || !fs_extra_1.default.statSync(markerPath).isFile()) {
|
|
2047
|
+
const projectName = path_1.default.basename(targetDir);
|
|
2048
|
+
fs_extra_1.default.writeFileSync(markerPath, JSON.stringify({
|
|
2049
|
+
name: projectName,
|
|
2050
|
+
createdAt: new Date().toISOString(),
|
|
2051
|
+
version: '0.2.0',
|
|
2052
|
+
adoptedBy: 'chimerai add',
|
|
2053
|
+
}, null, 2));
|
|
2054
|
+
// Register in global registry
|
|
2055
|
+
const { registerProject } = await import('../utils.js');
|
|
2056
|
+
registerProject(projectName, targetDir);
|
|
2057
|
+
console.log(chalk_1.default.green(` â Marked as ChimerAI project (.chimerai created)\n`));
|
|
2058
|
+
}
|
|
2059
|
+
// Check Next.js version â templates require Next.js 15+
|
|
2060
|
+
const project = await (0, scanner_js_1.scanProject)(targetDir);
|
|
2061
|
+
if (project.nextVersion) {
|
|
2062
|
+
const match = project.nextVersion.match(/\d+/);
|
|
2063
|
+
const major = match ? parseInt(match[0], 10) : null;
|
|
2064
|
+
if (major && major < 15) {
|
|
2065
|
+
console.log(chalk_1.default.yellow(`\nâ ď¸ Next.js ${project.nextVersion} detected. ChimerAI CLI requires Next.js 15 or later.`));
|
|
2066
|
+
console.log(chalk_1.default.yellow(' Generated code uses async params (Promise<params>) which is not compatible with Next.js 14.'));
|
|
2067
|
+
console.log(chalk_1.default.yellow(' Please upgrade: npx @next/codemod@canary upgrade latest\n'));
|
|
2068
|
+
const { proceed } = await inquirer_1.default.prompt([
|
|
2069
|
+
{
|
|
2070
|
+
type: 'confirm',
|
|
2071
|
+
name: 'proceed',
|
|
2072
|
+
message: 'Continue anyway? (generated code may not compile)',
|
|
2073
|
+
default: false,
|
|
2074
|
+
},
|
|
2075
|
+
]);
|
|
2076
|
+
if (!proceed) {
|
|
2077
|
+
console.log(chalk_1.default.gray('Aborted.'));
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
// AI Service modules â special handling (not simple file copy)
|
|
2083
|
+
const AI_COMPONENTS = {
|
|
2084
|
+
'ai-chat': addAiChat,
|
|
2085
|
+
rag: addRag, // Note: existing 'rag' config handles frontend proxy routes
|
|
2086
|
+
guardrails: addGuardrails,
|
|
2087
|
+
'ai-tools': addAiTools,
|
|
2088
|
+
};
|
|
2089
|
+
// Check if it's an AI service module
|
|
2090
|
+
if (AI_COMPONENTS[component]) {
|
|
2091
|
+
await AI_COMPONENTS[component](targetDir);
|
|
2092
|
+
console.log(chalk_1.default.bold.green(`\n⨠${component} added successfully!\n`));
|
|
2093
|
+
return;
|
|
2094
|
+
}
|
|
2095
|
+
// Check if component exists in standard configs
|
|
2096
|
+
const config = COMPONENT_CONFIGS[component];
|
|
2097
|
+
if (!config) {
|
|
2098
|
+
console.log(chalk_1.default.red(`â Unknown component: ${component}`));
|
|
2099
|
+
console.log(chalk_1.default.yellow('\nAvailable components:'));
|
|
2100
|
+
Object.values(COMPONENT_CONFIGS).forEach((cfg) => {
|
|
2101
|
+
if (cfg.disabled) {
|
|
2102
|
+
console.log(chalk_1.default.gray(` - ${cfg.name.padEnd(20)} ${cfg.description} (coming soon)`));
|
|
2103
|
+
}
|
|
2104
|
+
else {
|
|
2105
|
+
console.log(chalk_1.default.white(` - ${cfg.name.padEnd(20)} ${cfg.description}`));
|
|
2106
|
+
}
|
|
2107
|
+
});
|
|
2108
|
+
// Also show AI modules
|
|
2109
|
+
console.log(chalk_1.default.yellow('\nAI Service modules:'));
|
|
2110
|
+
console.log(chalk_1.default.white(' - ai-chat AI chat with streaming (installs core + chat services)'));
|
|
2111
|
+
console.log(chalk_1.default.white(' - rag RAG pipeline with vector search (requires ai-chat)'));
|
|
2112
|
+
console.log(chalk_1.default.white(' - guardrails Content moderation & safety guardrails'));
|
|
2113
|
+
console.log(chalk_1.default.white(' - ai-tools AI tools (web search, NLP, vision, etc.)'));
|
|
2114
|
+
(0, utils_js_1.handleCliError)(`Unknown component: ${component}`);
|
|
2115
|
+
}
|
|
2116
|
+
// Check if component is disabled (not yet implemented)
|
|
2117
|
+
if (config.disabled) {
|
|
2118
|
+
console.log(chalk_1.default.yellow(`â ď¸ ${config.displayName} is not yet available.`));
|
|
2119
|
+
console.log(chalk_1.default.gray(` ${config.disabled}`));
|
|
2120
|
+
console.log(chalk_1.default.cyan('\n Tip: Use "chimerai add admin-dashboard" for the full admin panel with RBAC.'));
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
// Add component using generic function
|
|
2124
|
+
await addComponent(config, targetDir);
|
|
2125
|
+
console.log(chalk_1.default.bold.green(`\n⨠${component} added successfully!\n`));
|
|
2126
|
+
}
|