@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.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +293 -0
  3. package/dist/cli.d.ts +7 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +317 -0
  6. package/dist/commands/add.d.ts +11 -0
  7. package/dist/commands/add.d.ts.map +1 -0
  8. package/dist/commands/add.js +2126 -0
  9. package/dist/commands/create.d.ts +12 -0
  10. package/dist/commands/create.d.ts.map +1 -0
  11. package/dist/commands/create.js +1703 -0
  12. package/dist/commands/deploy.d.ts +11 -0
  13. package/dist/commands/deploy.d.ts.map +1 -0
  14. package/dist/commands/deploy.js +219 -0
  15. package/dist/commands/dev.d.ts +17 -0
  16. package/dist/commands/dev.d.ts.map +1 -0
  17. package/dist/commands/dev.js +206 -0
  18. package/dist/commands/doctor.d.ts +11 -0
  19. package/dist/commands/doctor.d.ts.map +1 -0
  20. package/dist/commands/doctor.js +728 -0
  21. package/dist/commands/generate.d.ts +19 -0
  22. package/dist/commands/generate.d.ts.map +1 -0
  23. package/dist/commands/generate.js +429 -0
  24. package/dist/commands/init.d.ts +11 -0
  25. package/dist/commands/init.d.ts.map +1 -0
  26. package/dist/commands/init.js +269 -0
  27. package/dist/commands/list.d.ts +12 -0
  28. package/dist/commands/list.d.ts.map +1 -0
  29. package/dist/commands/list.js +328 -0
  30. package/dist/commands/migrate.d.ts +14 -0
  31. package/dist/commands/migrate.d.ts.map +1 -0
  32. package/dist/commands/migrate.js +197 -0
  33. package/dist/commands/plugin.d.ts +10 -0
  34. package/dist/commands/plugin.d.ts.map +1 -0
  35. package/dist/commands/plugin.js +239 -0
  36. package/dist/commands/remove.d.ts +11 -0
  37. package/dist/commands/remove.d.ts.map +1 -0
  38. package/dist/commands/remove.js +472 -0
  39. package/dist/commands/secret.d.ts +12 -0
  40. package/dist/commands/secret.d.ts.map +1 -0
  41. package/dist/commands/secret.js +102 -0
  42. package/dist/commands/setup.d.ts +9 -0
  43. package/dist/commands/setup.d.ts.map +1 -0
  44. package/dist/commands/setup.js +788 -0
  45. package/dist/commands/update.d.ts +14 -0
  46. package/dist/commands/update.d.ts.map +1 -0
  47. package/dist/commands/update.js +211 -0
  48. package/dist/commands/use.d.ts +9 -0
  49. package/dist/commands/use.d.ts.map +1 -0
  50. package/dist/commands/use.js +51 -0
  51. package/dist/index.d.ts +22 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +45 -0
  54. package/dist/license.d.ts +55 -0
  55. package/dist/license.d.ts.map +1 -0
  56. package/dist/license.js +258 -0
  57. package/dist/scanner.d.ts +31 -0
  58. package/dist/scanner.d.ts.map +1 -0
  59. package/dist/scanner.js +113 -0
  60. package/dist/schema-manager.d.ts +26 -0
  61. package/dist/schema-manager.d.ts.map +1 -0
  62. package/dist/schema-manager.js +132 -0
  63. package/dist/templates/admin.d.ts +49 -0
  64. package/dist/templates/admin.d.ts.map +1 -0
  65. package/dist/templates/admin.js +1358 -0
  66. package/dist/templates/ai-routes.d.ts +17 -0
  67. package/dist/templates/ai-routes.d.ts.map +1 -0
  68. package/dist/templates/ai-routes.js +1130 -0
  69. package/dist/templates/ai-service-tools.d.ts +22 -0
  70. package/dist/templates/ai-service-tools.d.ts.map +1 -0
  71. package/dist/templates/ai-service-tools.js +1424 -0
  72. package/dist/templates/ai-service.d.ts +66 -0
  73. package/dist/templates/ai-service.d.ts.map +1 -0
  74. package/dist/templates/ai-service.js +2202 -0
  75. package/dist/templates/api-routes.d.ts +108 -0
  76. package/dist/templates/api-routes.d.ts.map +1 -0
  77. package/dist/templates/api-routes.js +1219 -0
  78. package/dist/templates/auth.d.ts +48 -0
  79. package/dist/templates/auth.d.ts.map +1 -0
  80. package/dist/templates/auth.js +381 -0
  81. package/dist/templates/billing.d.ts +44 -0
  82. package/dist/templates/billing.d.ts.map +1 -0
  83. package/dist/templates/billing.js +551 -0
  84. package/dist/templates/chat.d.ts +63 -0
  85. package/dist/templates/chat.d.ts.map +1 -0
  86. package/dist/templates/chat.js +1979 -0
  87. package/dist/templates/components.d.ts +22 -0
  88. package/dist/templates/components.d.ts.map +1 -0
  89. package/dist/templates/components.js +672 -0
  90. package/dist/templates/config.d.ts +6 -0
  91. package/dist/templates/config.d.ts.map +1 -0
  92. package/dist/templates/config.js +86 -0
  93. package/dist/templates/docker.d.ts +25 -0
  94. package/dist/templates/docker.d.ts.map +1 -0
  95. package/dist/templates/docker.js +165 -0
  96. package/dist/templates/gdpr.d.ts +16 -0
  97. package/dist/templates/gdpr.d.ts.map +1 -0
  98. package/dist/templates/gdpr.js +259 -0
  99. package/dist/templates/index.d.ts +77 -0
  100. package/dist/templates/index.d.ts.map +1 -0
  101. package/dist/templates/index.js +339 -0
  102. package/dist/templates/layout.d.ts +67 -0
  103. package/dist/templates/layout.d.ts.map +1 -0
  104. package/dist/templates/layout.js +670 -0
  105. package/dist/templates/mfa.d.ts +23 -0
  106. package/dist/templates/mfa.d.ts.map +1 -0
  107. package/dist/templates/mfa.js +353 -0
  108. package/dist/templates/middleware.d.ts +12 -0
  109. package/dist/templates/middleware.d.ts.map +1 -0
  110. package/dist/templates/middleware.js +116 -0
  111. package/dist/templates/prisma.d.ts +35 -0
  112. package/dist/templates/prisma.d.ts.map +1 -0
  113. package/dist/templates/prisma.js +724 -0
  114. package/dist/templates/provider-routes.d.ts +21 -0
  115. package/dist/templates/provider-routes.d.ts.map +1 -0
  116. package/dist/templates/provider-routes.js +1203 -0
  117. package/dist/templates/rag.d.ts +48 -0
  118. package/dist/templates/rag.d.ts.map +1 -0
  119. package/dist/templates/rag.js +532 -0
  120. package/dist/templates/widget.d.ts +64 -0
  121. package/dist/templates/widget.d.ts.map +1 -0
  122. package/dist/templates/widget.js +1360 -0
  123. package/dist/utils/provider-db.d.ts +63 -0
  124. package/dist/utils/provider-db.d.ts.map +1 -0
  125. package/dist/utils/provider-db.js +300 -0
  126. package/dist/utils.d.ts +78 -0
  127. package/dist/utils.d.ts.map +1 -0
  128. package/dist/utils.js +330 -0
  129. 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
+ }