@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,1219 @@
1
+ "use strict";
2
+ /**
3
+ * API route templates for admin and provider management
4
+ * Generates CRUD endpoints for users, roles, and model providers
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.generateAdminUsersRoute = generateAdminUsersRoute;
8
+ exports.generateAdminUsersIdRoute = generateAdminUsersIdRoute;
9
+ exports.generateAdminRolesRoute = generateAdminRolesRoute;
10
+ exports.generateAdminRolesIdRoute = generateAdminRolesIdRoute;
11
+ exports.generateProvidersRoute = generateProvidersRoute;
12
+ exports.generatePromptsRoute = generatePromptsRoute;
13
+ exports.generatePromptsIdRoute = generatePromptsIdRoute;
14
+ exports.generatePromptsSetDefaultRoute = generatePromptsSetDefaultRoute;
15
+ exports.generateHealthRoute = generateHealthRoute;
16
+ exports.generateModelsRoute = generateModelsRoute;
17
+ exports.generateV1ModelsRoute = generateV1ModelsRoute;
18
+ exports.generateAuditLogRoute = generateAuditLogRoute;
19
+ exports.generateGdprDataExportRoute = generateGdprDataExportRoute;
20
+ exports.generateGdprAccountDeleteRoute = generateGdprAccountDeleteRoute;
21
+ exports.generateAdminSettingsRoute = generateAdminSettingsRoute;
22
+ exports.generateAppSettingsRoute = generateAppSettingsRoute;
23
+ /**
24
+ * Generates the admin users API route (GET, POST)
25
+ * GET: Lists all users with permission check
26
+ * POST: Creates new user with password hashing
27
+ * @returns TypeScript content for app/api/admin/users/route.ts
28
+ */
29
+ function generateAdminUsersRoute() {
30
+ return `// @chimerai component=AdminUsersRoute version=1.0
31
+ // @ts-nocheck
32
+ import { NextRequest, NextResponse } from 'next/server';
33
+ import { prisma } from '@/lib/prisma';
34
+ import bcrypt from 'bcryptjs';
35
+ import { requirePermission } from '@/lib/auth/require-permission';
36
+ import { logAuditAction } from '@/lib/audit';
37
+ import { getServerSession } from 'next-auth';
38
+ import { authOptions } from '@/lib/auth';
39
+
40
+ export async function GET(req: NextRequest) {
41
+ const permissionError = await requirePermission('users:read');
42
+ if (permissionError) return permissionError;
43
+
44
+ try {
45
+ const users = await prisma.user.findMany({
46
+ select: {
47
+ id: true,
48
+ email: true,
49
+ name: true,
50
+ image: true,
51
+ emailVerified: true,
52
+ createdAt: true,
53
+ roles: {
54
+ select: {
55
+ role: {
56
+ select: { id: true, name: true }
57
+ }
58
+ }
59
+ }
60
+ },
61
+ orderBy: { email: 'asc' }
62
+ });
63
+
64
+ // Flatten roles for easier frontend consumption
65
+ const usersWithRoles = users.map(u => ({
66
+ ...u,
67
+ roles: u.roles.map((r: any) => r.role),
68
+ }));
69
+
70
+ return NextResponse.json(usersWithRoles);
71
+ } catch (error) {
72
+ console.error('GET /api/admin/users error:', error);
73
+ return NextResponse.json({ error: 'Failed to fetch users' }, { status: 500 });
74
+ }
75
+ }
76
+
77
+ export async function POST(req: NextRequest) {
78
+ const permissionError = await requirePermission('users:write');
79
+ if (permissionError) return permissionError;
80
+
81
+ try {
82
+ const body = await req.json();
83
+ const { email, name, password, roleIds } = body;
84
+
85
+ if (!email || !password) {
86
+ return NextResponse.json({ error: 'Email and password required' }, { status: 400 });
87
+ }
88
+
89
+ const hashedPassword = await bcrypt.hash(password, 10);
90
+
91
+ const user = await prisma.user.create({
92
+ data: {
93
+ email,
94
+ name: name || null,
95
+ password: hashedPassword
96
+ }
97
+ });
98
+
99
+ // Assign roles if provided
100
+ if (Array.isArray(roleIds) && roleIds.length > 0) {
101
+ await prisma.userRole.createMany({
102
+ data: roleIds.map((roleId: string) => ({
103
+ userId: user.id,
104
+ roleId,
105
+ })),
106
+ skipDuplicates: true,
107
+ });
108
+ }
109
+
110
+ // Audit log
111
+ const session = await getServerSession(authOptions);
112
+ if (session?.user?.id) {
113
+ await logAuditAction({
114
+ action: 'user.create',
115
+ userId: session.user.id,
116
+ targetType: 'user',
117
+ targetId: user.id,
118
+ metadata: { email: user.email, name: user.name },
119
+ });
120
+ }
121
+
122
+ return NextResponse.json({ id: user.id, email: user.email, name: user.name });
123
+ } catch (error) {
124
+ console.error('POST /api/admin/users error:', error);
125
+ return NextResponse.json({
126
+ error: 'Failed to create user',
127
+ details: error instanceof Error ? error.message : String(error)
128
+ }, { status: 500 });
129
+ }
130
+ }
131
+ `;
132
+ }
133
+ /**
134
+ * Generates the admin users/:id API route (PUT, DELETE)
135
+ * PUT: Updates user email and name
136
+ * DELETE: Deletes user by ID
137
+ * @returns TypeScript content for app/api/admin/users/[id]/route.ts
138
+ */
139
+ function generateAdminUsersIdRoute() {
140
+ return `// @chimerai component=AdminUsersIdRoute version=1.0
141
+ // @ts-nocheck
142
+ import { NextRequest, NextResponse } from 'next/server';
143
+ import { prisma } from '@/lib/prisma';
144
+ import { requirePermission } from '@/lib/auth/require-permission';
145
+ import { logAuditAction } from '@/lib/audit';
146
+ import { getServerSession } from 'next-auth';
147
+ import { authOptions } from '@/lib/auth';
148
+
149
+ export async function PATCH(
150
+ req: NextRequest,
151
+ { params }: { params: Promise<{ id: string }> }
152
+ ) {
153
+ const { id } = await params;
154
+ const permissionError = await requirePermission('users:write');
155
+ if (permissionError) return permissionError;
156
+
157
+ try {
158
+ const body = await req.json();
159
+ const { email, name, roleIds } = body;
160
+
161
+ const user = await prisma.user.update({
162
+ where: { id: id },
163
+ data: {
164
+ email,
165
+ name: name || null
166
+ }
167
+ });
168
+
169
+ // Sync roles if provided
170
+ if (Array.isArray(roleIds)) {
171
+ await prisma.userRole.deleteMany({ where: { userId: id } });
172
+ if (roleIds.length > 0) {
173
+ await prisma.userRole.createMany({
174
+ data: roleIds.map((roleId: string) => ({
175
+ userId: id,
176
+ roleId,
177
+ })),
178
+ skipDuplicates: true,
179
+ });
180
+ }
181
+ }
182
+
183
+ // Audit log
184
+ const session = await getServerSession(authOptions);
185
+ if (session?.user?.id) {
186
+ await logAuditAction({
187
+ action: 'user.update',
188
+ userId: session.user.id,
189
+ targetType: 'user',
190
+ targetId: id,
191
+ metadata: { email, name },
192
+ });
193
+ }
194
+
195
+ return NextResponse.json({ id: user.id, email: user.email, name: user.name });
196
+ } catch (error) {
197
+ console.error('PUT /api/admin/users/[id] error:', error);
198
+ return NextResponse.json({
199
+ error: 'Failed to update user',
200
+ details: error instanceof Error ? error.message : String(error)
201
+ }, { status: 500 });
202
+ }
203
+ }
204
+
205
+ export async function DELETE(
206
+ req: NextRequest,
207
+ { params }: { params: Promise<{ id: string }> }
208
+ ) {
209
+ const { id } = await params;
210
+ const permissionError = await requirePermission('users:delete');
211
+ if (permissionError) return permissionError;
212
+
213
+ try {
214
+ await prisma.user.delete({
215
+ where: { id: id }
216
+ });
217
+
218
+ // Audit log
219
+ const session = await getServerSession(authOptions);
220
+ if (session?.user?.id) {
221
+ await logAuditAction({
222
+ action: 'user.delete',
223
+ userId: session.user.id,
224
+ targetType: 'user',
225
+ targetId: id,
226
+ });
227
+ }
228
+
229
+ return NextResponse.json({ success: true });
230
+ } catch (error) {
231
+ console.error('DELETE /api/admin/users/[id] error:', error);
232
+ return NextResponse.json({
233
+ error: 'Failed to delete user',
234
+ details: error instanceof Error ? error.message : String(error)
235
+ }, { status: 500 });
236
+ }
237
+ }
238
+ `;
239
+ }
240
+ /**
241
+ * Generates the admin roles API route (GET, POST)
242
+ * GET: Lists all roles with permission check
243
+ * POST: Creates new role with permissions array
244
+ * @returns TypeScript content for app/api/admin/roles/route.ts
245
+ */
246
+ function generateAdminRolesRoute() {
247
+ return `// @chimerai component=AdminRolesRoute version=1.0
248
+ import { NextRequest, NextResponse } from 'next/server';
249
+ import { prisma } from '@/lib/prisma';
250
+ import { requirePermission } from '@/lib/auth/require-permission';
251
+ import { logAuditAction } from '@/lib/audit';
252
+ import { getServerSession } from 'next-auth';
253
+ import { authOptions } from '@/lib/auth';
254
+
255
+ export async function GET(req: NextRequest) {
256
+ const permissionError = await requirePermission('roles:read');
257
+ if (permissionError) return permissionError;
258
+
259
+ try {
260
+ const roles = await prisma.role.findMany({
261
+ orderBy: { name: 'asc' }
262
+ });
263
+
264
+ return NextResponse.json(roles);
265
+ } catch (error) {
266
+ console.error('GET /api/admin/roles error:', error);
267
+ return NextResponse.json({ error: 'Failed to fetch roles' }, { status: 500 });
268
+ }
269
+ }
270
+
271
+ export async function POST(req: NextRequest) {
272
+ const permissionError = await requirePermission('roles:write');
273
+ if (permissionError) return permissionError;
274
+
275
+ try {
276
+ const body = await req.json();
277
+ const { name, description, permissions } = body;
278
+
279
+ if (!name) {
280
+ return NextResponse.json({ error: 'Name required' }, { status: 400 });
281
+ }
282
+
283
+ const role = await prisma.role.create({
284
+ data: {
285
+ name,
286
+ description: description || null,
287
+ permissions: permissions || []
288
+ }
289
+ });
290
+
291
+ // Audit log
292
+ const session = await getServerSession(authOptions);
293
+ if (session?.user?.id) {
294
+ await logAuditAction({
295
+ action: 'role.create',
296
+ userId: session.user.id,
297
+ targetType: 'role',
298
+ targetId: role.id,
299
+ metadata: { name: role.name },
300
+ });
301
+ }
302
+
303
+ return NextResponse.json(role);
304
+ } catch (error) {
305
+ console.error('POST /api/admin/roles error:', error);
306
+ return NextResponse.json({
307
+ error: 'Failed to create role',
308
+ details: error instanceof Error ? error.message : String(error)
309
+ }, { status: 500 });
310
+ }
311
+ }
312
+ `;
313
+ }
314
+ /**
315
+ * Generates the admin roles/:id API route (PUT, DELETE)
316
+ * PUT: Updates role name, description, and permissions
317
+ * DELETE: Deletes role by ID
318
+ * @returns TypeScript content for app/api/admin/roles/[id]/route.ts
319
+ */
320
+ function generateAdminRolesIdRoute() {
321
+ return `// @chimerai component=AdminRolesIdRoute version=1.0
322
+ import { NextRequest, NextResponse } from 'next/server';
323
+ import { prisma } from '@/lib/prisma';
324
+ import { requirePermission } from '@/lib/auth/require-permission';
325
+ import { logAuditAction } from '@/lib/audit';
326
+ import { getServerSession } from 'next-auth';
327
+ import { authOptions } from '@/lib/auth';
328
+
329
+ export async function PUT(
330
+ req: NextRequest,
331
+ { params }: { params: Promise<{ id: string }> }
332
+ ) {
333
+ const { id } = await params;
334
+ const permissionError = await requirePermission('roles:write');
335
+ if (permissionError) return permissionError;
336
+
337
+ try {
338
+ const body = await req.json();
339
+ const { name, description, permissions } = body;
340
+
341
+ const role = await prisma.role.update({
342
+ where: { id: id },
343
+ data: {
344
+ name,
345
+ description: description || null,
346
+ permissions: permissions || []
347
+ }
348
+ });
349
+
350
+ // Audit log
351
+ const session = await getServerSession(authOptions);
352
+ if (session?.user?.id) {
353
+ await logAuditAction({
354
+ action: 'role.update',
355
+ userId: session.user.id,
356
+ targetType: 'role',
357
+ targetId: role.id,
358
+ metadata: { name: role.name },
359
+ });
360
+ }
361
+
362
+ return NextResponse.json(role);
363
+ } catch (error) {
364
+ console.error('PUT /api/admin/roles/[id] error:', error);
365
+ return NextResponse.json({
366
+ error: 'Failed to update role',
367
+ details: error instanceof Error ? error.message : String(error)
368
+ }, { status: 500 });
369
+ }
370
+ }
371
+
372
+ export async function DELETE(
373
+ req: NextRequest,
374
+ { params }: { params: Promise<{ id: string }> }
375
+ ) {
376
+ const { id } = await params;
377
+ const permissionError = await requirePermission('roles:delete');
378
+ if (permissionError) return permissionError;
379
+
380
+ try {
381
+ await prisma.role.delete({
382
+ where: { id: id }
383
+ });
384
+
385
+ // Audit log
386
+ const session = await getServerSession(authOptions);
387
+ if (session?.user?.id) {
388
+ await logAuditAction({
389
+ action: 'role.delete',
390
+ userId: session.user.id,
391
+ targetType: 'role',
392
+ targetId: id,
393
+ });
394
+ }
395
+
396
+ return NextResponse.json({ success: true });
397
+ } catch (error) {
398
+ console.error('DELETE /api/admin/roles/[id] error:', error);
399
+ return NextResponse.json({
400
+ error: 'Failed to delete role',
401
+ details: error instanceof Error ? error.message : String(error)
402
+ }, { status: 500 });
403
+ }
404
+ }
405
+ `;
406
+ }
407
+ /**
408
+ * Generates the model providers API route (GET, POST, PUT, DELETE)
409
+ * Handles CRUD for AI model provider configurations
410
+ * Includes API key encryption/decryption for security
411
+ * @returns TypeScript content for app/api/providers/route.ts
412
+ */
413
+ function generateProvidersRoute() {
414
+ return `// @chimerai component=ProvidersRoute version=1.0
415
+ // Model Providers API Route
416
+ import { NextRequest, NextResponse } from 'next/server';
417
+ import { getServerSession } from 'next-auth';
418
+ import { authOptions } from '@/lib/auth';
419
+ import { prisma } from '@/lib/prisma';
420
+ import { encrypt, decrypt } from '@/lib/encryption';
421
+
422
+ export async function GET(req: NextRequest) {
423
+ const session = await getServerSession(authOptions);
424
+
425
+ if (!session?.user?.id) {
426
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
427
+ }
428
+
429
+ try {
430
+ const providers = await prisma.modelProvider.findMany({
431
+ where: { userId: session.user.id },
432
+ orderBy: { createdAt: 'desc' },
433
+ });
434
+
435
+ const decryptedProviders = providers.map((p) => ({
436
+ ...p,
437
+ apiKey: p.apiKey ? decrypt(p.apiKey) : undefined,
438
+ }));
439
+
440
+ return NextResponse.json(decryptedProviders);
441
+ } catch (error: any) {
442
+ console.error('Error fetching providers:', error);
443
+ return NextResponse.json({ error: 'Failed to fetch providers' }, { status: 500 });
444
+ }
445
+ }
446
+
447
+ export async function POST(req: NextRequest) {
448
+ const session = await getServerSession(authOptions);
449
+
450
+ if (!session?.user?.id) {
451
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
452
+ }
453
+
454
+ try {
455
+ const body = await req.json();
456
+ const encryptedKey = body.apiKey ? encrypt(body.apiKey) : undefined;
457
+
458
+ const provider = await prisma.modelProvider.create({
459
+ data: {
460
+ ...body,
461
+ apiKey: encryptedKey,
462
+ userId: session.user.id,
463
+ },
464
+ });
465
+
466
+ return NextResponse.json(provider, { status: 201 });
467
+ } catch (error: any) {
468
+ console.error('Error creating provider:', error);
469
+ return NextResponse.json({ error: 'Failed to create provider' }, { status: 500 });
470
+ }
471
+ }
472
+
473
+ export async function PUT(req: NextRequest) {
474
+ const session = await getServerSession(authOptions);
475
+
476
+ if (!session?.user?.id) {
477
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
478
+ }
479
+
480
+ try {
481
+ const body = await req.json();
482
+ const { id, apiKey, ...updateData } = body;
483
+
484
+ const encryptedKey = apiKey ? encrypt(apiKey) : undefined;
485
+
486
+ const provider = await prisma.modelProvider.update({
487
+ where: {
488
+ id,
489
+ userId: session.user.id,
490
+ },
491
+ data: {
492
+ ...updateData,
493
+ ...(encryptedKey && { apiKey: encryptedKey }),
494
+ },
495
+ });
496
+
497
+ return NextResponse.json(provider);
498
+ } catch (error: any) {
499
+ console.error('Error updating provider:', error);
500
+ return NextResponse.json({ error: 'Failed to update provider' }, { status: 500 });
501
+ }
502
+ }
503
+
504
+ export async function DELETE(req: NextRequest) {
505
+ const session = await getServerSession(authOptions);
506
+
507
+ if (!session?.user?.id) {
508
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
509
+ }
510
+
511
+ try {
512
+ const body = await req.json();
513
+ const { id } = body;
514
+
515
+ await prisma.modelProvider.delete({
516
+ where: {
517
+ id,
518
+ userId: session.user.id,
519
+ },
520
+ });
521
+
522
+ return NextResponse.json({ success: true });
523
+ } catch (error: any) {
524
+ console.error('Error deleting provider:', error);
525
+ return NextResponse.json({ error: 'Failed to delete provider' }, { status: 500 });
526
+ }
527
+ }
528
+ `;
529
+ }
530
+ /**
531
+ * Generates the prompt templates API route (GET, POST)
532
+ * v2: JSON serialization fix, user scoping, auto-extract variables
533
+ * @returns TypeScript content for app/api/prompts/route.ts
534
+ */
535
+ function generatePromptsRoute() {
536
+ return `// @chimerai component=PromptsRoute version=2.0
537
+ import { NextRequest, NextResponse } from 'next/server';
538
+ import { getServerSession } from 'next-auth';
539
+ import { authOptions } from '@/lib/auth';
540
+ import { prisma } from '@/lib/prisma';
541
+
542
+ /** Parse JSON string fields (variables, tags) from SQLite storage back to arrays */
543
+ function parseTemplate(t: any) {
544
+ return {
545
+ ...t,
546
+ variables: (() => { try { return JSON.parse(t.variables || '[]'); } catch { return []; } })(),
547
+ tags: (() => { try { return JSON.parse(t.tags || '[]'); } catch { return []; } })(),
548
+ };
549
+ }
550
+
551
+ /** Auto-extract {{variable}} names from template content */
552
+ function extractVariables(content: string): string[] {
553
+ const matches = content.match(/{{(\\w+)}}/g) || [];
554
+ return [...new Set(matches.map((m: string) => m.replace(/{{|}}/g, '')))];
555
+ }
556
+
557
+ export async function GET(req: NextRequest) {
558
+ const session = await getServerSession(authOptions);
559
+ if (!session?.user?.id) {
560
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
561
+ }
562
+
563
+ try {
564
+ const { searchParams } = new URL(req.url);
565
+ const category = searchParams.get('category');
566
+ const search = searchParams.get('search');
567
+
568
+ // User sees their own templates + system/seed templates (createdBy: null)
569
+ const andClauses: any[] = [
570
+ {
571
+ OR: [
572
+ { createdBy: session.user.id },
573
+ { createdBy: null },
574
+ ],
575
+ },
576
+ ];
577
+ if (category && category !== 'all') {
578
+ andClauses.push({ category });
579
+ }
580
+ if (search) {
581
+ andClauses.push({
582
+ OR: [
583
+ { name: { contains: search } },
584
+ { description: { contains: search } },
585
+ ],
586
+ });
587
+ }
588
+
589
+ const raw = await prisma.promptTemplate.findMany({
590
+ where: { AND: andClauses },
591
+ orderBy: [{ isDefault: 'desc' }, { updatedAt: 'desc' }],
592
+ });
593
+
594
+ return NextResponse.json(raw.map(parseTemplate));
595
+ } catch (error: any) {
596
+ console.error('Error fetching prompts:', error);
597
+ return NextResponse.json({ error: 'Failed to fetch prompts' }, { status: 500 });
598
+ }
599
+ }
600
+
601
+ export async function POST(req: NextRequest) {
602
+ const session = await getServerSession(authOptions);
603
+ if (!session?.user?.id) {
604
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
605
+ }
606
+
607
+ try {
608
+ const body = await req.json();
609
+
610
+ const template = await prisma.promptTemplate.create({
611
+ data: {
612
+ name: body.name,
613
+ category: body.category,
614
+ description: body.description || null,
615
+ content: body.content,
616
+ variables: JSON.stringify(extractVariables(body.content || '')),
617
+ language: body.language || 'en',
618
+ tags: JSON.stringify(body.tags || []),
619
+ createdBy: session.user.id,
620
+ },
621
+ });
622
+
623
+ return NextResponse.json(parseTemplate(template), { status: 201 });
624
+ } catch (error: any) {
625
+ console.error('Error creating prompt:', error);
626
+ if (error.code === 'P2002') {
627
+ return NextResponse.json({ error: 'A prompt with this name already exists' }, { status: 409 });
628
+ }
629
+ return NextResponse.json({ error: 'Failed to create prompt' }, { status: 500 });
630
+ }
631
+ }
632
+ `;
633
+ }
634
+ /**
635
+ * Generates the prompt templates ID API route (GET, PUT, DELETE)
636
+ * v2: JSON serialization fix, ownership checks (user can only edit/delete own templates)
637
+ * @returns TypeScript content for app/api/prompts/[id]/route.ts
638
+ */
639
+ function generatePromptsIdRoute() {
640
+ return `// @chimerai component=PromptsIdRoute version=2.0
641
+ import { NextRequest, NextResponse } from 'next/server';
642
+ import { getServerSession } from 'next-auth';
643
+ import { authOptions } from '@/lib/auth';
644
+ import { prisma } from '@/lib/prisma';
645
+
646
+ function parseTemplate(t: any) {
647
+ return {
648
+ ...t,
649
+ variables: (() => { try { return JSON.parse(t.variables || '[]'); } catch { return []; } })(),
650
+ tags: (() => { try { return JSON.parse(t.tags || '[]'); } catch { return []; } })(),
651
+ };
652
+ }
653
+
654
+ function extractVariables(content: string): string[] {
655
+ const matches = content.match(/{{(\\w+)}}/g) || [];
656
+ return [...new Set(matches.map((m: string) => m.replace(/{{|}}/g, '')))];
657
+ }
658
+
659
+ export async function GET(
660
+ req: NextRequest,
661
+ { params }: { params: Promise<{ id: string }> }
662
+ ) {
663
+ const { id } = await params;
664
+ const session = await getServerSession(authOptions);
665
+ if (!session?.user?.id) {
666
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
667
+ }
668
+
669
+ try {
670
+ const template = await prisma.promptTemplate.findFirst({
671
+ where: {
672
+ id,
673
+ OR: [{ createdBy: session.user.id }, { createdBy: null }],
674
+ },
675
+ });
676
+ if (!template) {
677
+ return NextResponse.json({ error: 'Prompt not found' }, { status: 404 });
678
+ }
679
+ return NextResponse.json(parseTemplate(template));
680
+ } catch (error: any) {
681
+ console.error('Error fetching prompt:', error);
682
+ return NextResponse.json({ error: 'Failed to fetch prompt' }, { status: 500 });
683
+ }
684
+ }
685
+
686
+ export async function PUT(
687
+ req: NextRequest,
688
+ { params }: { params: Promise<{ id: string }> }
689
+ ) {
690
+ const { id } = await params;
691
+ const session = await getServerSession(authOptions);
692
+ if (!session?.user?.id) {
693
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
694
+ }
695
+
696
+ try {
697
+ const body = await req.json();
698
+
699
+ // Ownership check — only the creator can edit (system templates have createdBy: null)
700
+ const existing = await prisma.promptTemplate.findFirst({
701
+ where: { id, createdBy: session.user.id },
702
+ });
703
+ if (!existing) {
704
+ return NextResponse.json({ error: 'Not found or not authorized' }, { status: 404 });
705
+ }
706
+
707
+ const template = await prisma.promptTemplate.update({
708
+ where: { id },
709
+ data: {
710
+ name: body.name,
711
+ category: body.category,
712
+ description: body.description ?? null,
713
+ content: body.content,
714
+ variables: JSON.stringify(extractVariables(body.content || '')),
715
+ language: body.language,
716
+ tags: JSON.stringify(body.tags || []),
717
+ version: { increment: 1 },
718
+ },
719
+ });
720
+
721
+ return NextResponse.json(parseTemplate(template));
722
+ } catch (error: any) {
723
+ console.error('Error updating prompt:', error);
724
+ if (error.code === 'P2025') {
725
+ return NextResponse.json({ error: 'Prompt not found' }, { status: 404 });
726
+ }
727
+ return NextResponse.json({ error: 'Failed to update prompt' }, { status: 500 });
728
+ }
729
+ }
730
+
731
+ export async function DELETE(
732
+ req: NextRequest,
733
+ { params }: { params: Promise<{ id: string }> }
734
+ ) {
735
+ const { id } = await params;
736
+ const session = await getServerSession(authOptions);
737
+ if (!session?.user?.id) {
738
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
739
+ }
740
+
741
+ try {
742
+ // Only the creator can delete — system templates (createdBy: null) are protected
743
+ const existing = await prisma.promptTemplate.findFirst({
744
+ where: { id, createdBy: session.user.id },
745
+ });
746
+ if (!existing) {
747
+ return NextResponse.json({ error: 'Not found or not authorized' }, { status: 404 });
748
+ }
749
+
750
+ await prisma.promptTemplate.delete({ where: { id } });
751
+ return NextResponse.json({ success: true });
752
+ } catch (error: any) {
753
+ console.error('Error deleting prompt:', error);
754
+ if (error.code === 'P2025') {
755
+ return NextResponse.json({ error: 'Prompt not found' }, { status: 404 });
756
+ }
757
+ return NextResponse.json({ error: 'Failed to delete prompt' }, { status: 500 });
758
+ }
759
+ }
760
+ `;
761
+ }
762
+ /**
763
+ * Generates the set-default route for prompt templates
764
+ * POST /api/prompts/[id]/set-default — atomically sets one template as default for its category
765
+ * @returns TypeScript content for app/api/prompts/[id]/set-default/route.ts
766
+ */
767
+ function generatePromptsSetDefaultRoute() {
768
+ return `// @chimerai component=PromptsSetDefaultRoute version=1.0
769
+ import { NextRequest, NextResponse } from 'next/server';
770
+ import { getServerSession } from 'next-auth';
771
+ import { authOptions } from '@/lib/auth';
772
+ import { prisma } from '@/lib/prisma';
773
+
774
+ export async function POST(
775
+ req: NextRequest,
776
+ { params }: { params: Promise<{ id: string }> }
777
+ ) {
778
+ const { id } = await params;
779
+ const session = await getServerSession(authOptions);
780
+ if (!session?.user?.id) {
781
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
782
+ }
783
+
784
+ try {
785
+ // Find the template (user's own OR system template)
786
+ const template = await prisma.promptTemplate.findFirst({
787
+ where: {
788
+ id,
789
+ OR: [{ createdBy: session.user.id }, { createdBy: null }],
790
+ },
791
+ });
792
+ if (!template) {
793
+ return NextResponse.json({ error: 'Prompt not found' }, { status: 404 });
794
+ }
795
+
796
+ // Atomically: unset all defaults in this category, then set this one
797
+ await prisma.$transaction([
798
+ prisma.promptTemplate.updateMany({
799
+ where: { category: template.category, isDefault: true },
800
+ data: { isDefault: false },
801
+ }),
802
+ prisma.promptTemplate.update({
803
+ where: { id },
804
+ data: { isDefault: true },
805
+ }),
806
+ ]);
807
+
808
+ return NextResponse.json({ success: true });
809
+ } catch (error: any) {
810
+ console.error('Error setting default prompt:', error);
811
+ return NextResponse.json({ error: 'Failed to set default prompt' }, { status: 500 });
812
+ }
813
+ }
814
+ `;
815
+ }
816
+ /**
817
+ * Generates the /api/health endpoint for Docker healthchecks
818
+ * Returns a simple JSON status response
819
+ * @returns TypeScript content for app/api/health/route.ts
820
+ */
821
+ function generateHealthRoute() {
822
+ return `// @chimerai component=HealthRoute version=1.0
823
+ import { NextResponse } from 'next/server';
824
+
825
+ export async function GET() {
826
+ return NextResponse.json({ status: 'ok', timestamp: new Date().toISOString() });
827
+ }
828
+ `;
829
+ }
830
+ /**
831
+ * Generates the /api/models endpoint.
832
+ * Lists all models from active providers for the chat ModelSelector.
833
+ * @returns TypeScript content for app/api/models/route.ts
834
+ */
835
+ function generateModelsRoute() {
836
+ return `// @chimerai component=ModelsRoute version=1.0
837
+ import { NextResponse } from 'next/server';
838
+ import { getServerSession } from 'next-auth';
839
+ import { authOptions } from '@/lib/auth';
840
+ import { prisma } from '@/lib/prisma';
841
+
842
+ export async function GET() {
843
+ const session = await getServerSession(authOptions);
844
+ if (!session?.user) {
845
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
846
+ }
847
+
848
+ try {
849
+ const models = await (prisma as any).model.findMany({
850
+ where: {
851
+ provider: {
852
+ status: 'active',
853
+ },
854
+ },
855
+ include: {
856
+ provider: {
857
+ select: {
858
+ id: true,
859
+ name: true,
860
+ type: true,
861
+ },
862
+ },
863
+ },
864
+ orderBy: [
865
+ { provider: { priority: 'asc' } },
866
+ { name: 'asc' },
867
+ ],
868
+ });
869
+
870
+ // Filter to chat-capable models only (exclude embedding-only models)
871
+ const chatModels = models.filter((m: any) => {
872
+ const caps = Array.isArray(m.capabilities)
873
+ ? m.capabilities
874
+ : (() => { try { return JSON.parse(m.capabilities || '[]'); } catch { return []; } })();
875
+ return caps.includes('chat') || caps.includes('vision') || caps.length === 0;
876
+ });
877
+
878
+ const result = chatModels.map((m: any) => ({
879
+ id: m.id,
880
+ modelId: m.modelId,
881
+ name: m.name,
882
+ providerId: m.providerId,
883
+ providerType: m.provider.type,
884
+ contextWindow: m.contextWindow || 0,
885
+ inputCost: m.inputCost || 0,
886
+ outputCost: m.outputCost || 0,
887
+ capabilities: m.capabilities || [],
888
+ provider: m.provider,
889
+ }));
890
+
891
+ return NextResponse.json(result);
892
+ } catch (error) {
893
+ console.error('Failed to fetch models:', error);
894
+ return NextResponse.json({ error: 'Failed to fetch models' }, { status: 500 });
895
+ }
896
+ }
897
+ `;
898
+ }
899
+ /**
900
+ * Generates the /api/v1/models endpoint with dual-auth (Session OR API-Key).
901
+ * External apps and widgets can query available models via API-Key.
902
+ * Internal web UI can use it via session auth.
903
+ * @returns TypeScript content for app/api/v1/models/route.ts
904
+ */
905
+ function generateV1ModelsRoute() {
906
+ return `// @chimerai component=V1ModelsRoute version=1.0
907
+ import { NextRequest, NextResponse } from 'next/server';
908
+ import { resolveAuth } from '@/lib/auth/resolve-auth';
909
+ import { prisma } from '@/lib/prisma';
910
+
911
+ export const dynamic = 'force-dynamic';
912
+
913
+ export async function GET(request: NextRequest) {
914
+ try {
915
+ let auth;
916
+ try {
917
+ auth = await resolveAuth(request);
918
+ } catch (error: any) {
919
+ return NextResponse.json({ error: 'Unauthorized' }, { status: error.status || 401 });
920
+ }
921
+
922
+ // API-Key scope check: requires 'chat' or '*' scope
923
+ if (auth.authMethod === 'api-key' && auth.scopes) {
924
+ const hasChat = auth.scopes.includes('chat') || auth.scopes.includes('*') || auth.scopes.length === 0;
925
+ if (!hasChat) {
926
+ return NextResponse.json({ error: 'Insufficient scope. Required: chat' }, { status: 403 });
927
+ }
928
+ }
929
+
930
+ const models = await (prisma as any).model.findMany({
931
+ where: {
932
+ provider: {
933
+ status: 'active',
934
+ },
935
+ },
936
+ include: {
937
+ provider: {
938
+ select: {
939
+ id: true,
940
+ name: true,
941
+ type: true,
942
+ },
943
+ },
944
+ },
945
+ orderBy: [
946
+ { provider: { priority: 'asc' } },
947
+ { name: 'asc' },
948
+ ],
949
+ });
950
+
951
+ const result = models.map((m: any) => ({
952
+ id: m.id,
953
+ modelId: m.modelId,
954
+ name: m.name,
955
+ providerId: m.providerId,
956
+ providerType: m.provider.type,
957
+ contextWindow: m.contextWindow || 0,
958
+ inputCost: m.inputCost || 0,
959
+ outputCost: m.outputCost || 0,
960
+ capabilities: m.capabilities || [],
961
+ provider: m.provider,
962
+ }));
963
+
964
+ // Filter to chat-capable models only (exclude embedding-only models)
965
+ const chatModels = result.filter((m: any) => {
966
+ const caps = Array.isArray(m.capabilities)
967
+ ? m.capabilities
968
+ : (() => { try { return JSON.parse(m.capabilities || '[]'); } catch { return []; } })();
969
+ return caps.includes('chat') || caps.includes('vision') || caps.length === 0;
970
+ });
971
+
972
+ return NextResponse.json(chatModels);
973
+ } catch (error) {
974
+ console.error('Failed to fetch models:', error);
975
+ return NextResponse.json({ error: 'Failed to fetch models' }, { status: 500 });
976
+ }
977
+ }
978
+ `;
979
+ }
980
+ /**
981
+ * Generates the admin audit logs API route.
982
+ * GET with pagination and action filter.
983
+ * @returns TypeScript content for app/api/admin/audit-logs/route.ts
984
+ */
985
+ function generateAuditLogRoute() {
986
+ return `// @chimerai component=AuditLogRoute version=1.0
987
+ import { NextRequest, NextResponse } from 'next/server';
988
+ import { getServerSession } from 'next-auth';
989
+ import { authOptions } from '@/lib/auth';
990
+ import { prisma } from '@/lib/prisma';
991
+
992
+ export async function GET(request: NextRequest) {
993
+ const session = await getServerSession(authOptions);
994
+ if (!session?.user?.id) {
995
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
996
+ }
997
+
998
+ const { searchParams } = new URL(request.url);
999
+ const page = parseInt(searchParams.get('page') || '1', 10);
1000
+ const pageSize = Math.min(parseInt(searchParams.get('pageSize') || '25', 10), 100);
1001
+ const actionFilter = searchParams.get('action') || undefined;
1002
+
1003
+ const where: any = {};
1004
+ if (actionFilter) {
1005
+ where.action = actionFilter;
1006
+ }
1007
+
1008
+ const [logs, total] = await Promise.all([
1009
+ (prisma as any).auditLog.findMany({
1010
+ where,
1011
+ include: {
1012
+ user: {
1013
+ select: { name: true, email: true },
1014
+ },
1015
+ },
1016
+ orderBy: { createdAt: 'desc' },
1017
+ skip: (page - 1) * pageSize,
1018
+ take: pageSize,
1019
+ }),
1020
+ (prisma as any).auditLog.count({ where }),
1021
+ ]);
1022
+
1023
+ return NextResponse.json({ logs, total, page, pageSize });
1024
+ }
1025
+ `;
1026
+ }
1027
+ /**
1028
+ * Generates the GDPR data export route.
1029
+ * GET /api/user/data-export — returns all user data as JSON.
1030
+ * @returns TypeScript content for app/api/user/data-export/route.ts
1031
+ */
1032
+ function generateGdprDataExportRoute() {
1033
+ return `// @chimerai component=GdprDataExportRoute version=1.0
1034
+ import { NextResponse } from 'next/server';
1035
+ import { getServerSession } from 'next-auth';
1036
+ import { authOptions } from '@/lib/auth';
1037
+ import { exportUserData } from '@/lib/gdpr';
1038
+
1039
+ export async function GET() {
1040
+ const session = await getServerSession(authOptions);
1041
+ if (!session?.user?.id) {
1042
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
1043
+ }
1044
+
1045
+ const userId = session.user.id;
1046
+ const exportData = await exportUserData(userId);
1047
+
1048
+ return new NextResponse(JSON.stringify(exportData, null, 2), {
1049
+ headers: {
1050
+ 'Content-Type': 'application/json',
1051
+ 'Content-Disposition': \`attachment; filename="data-export-\${userId}.json"\`,
1052
+ },
1053
+ });
1054
+ }
1055
+ `;
1056
+ }
1057
+ /**
1058
+ * Generates the GDPR account deletion route.
1059
+ * DELETE /api/user/account — deletes user account with cascade.
1060
+ * @returns TypeScript content for app/api/user/account/route.ts
1061
+ */
1062
+ function generateGdprAccountDeleteRoute() {
1063
+ return `// @chimerai component=GdprAccountDeleteRoute version=1.0
1064
+ import { NextRequest, NextResponse } from 'next/server';
1065
+ import { getServerSession } from 'next-auth';
1066
+ import { authOptions } from '@/lib/auth';
1067
+ import { deleteUserData } from '@/lib/gdpr';
1068
+
1069
+ export async function DELETE(request: NextRequest) {
1070
+ const session = await getServerSession(authOptions);
1071
+ if (!session?.user?.id) {
1072
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
1073
+ }
1074
+
1075
+ const userId = session.user.id;
1076
+
1077
+ try {
1078
+ const body = await request.json();
1079
+ if (body.confirmation !== session.user.email) {
1080
+ return NextResponse.json(
1081
+ { error: 'Please confirm by typing your email address' },
1082
+ { status: 400 }
1083
+ );
1084
+ }
1085
+ } catch {
1086
+ return NextResponse.json(
1087
+ { error: 'Invalid request body. Send { confirmation: "your-email" }' },
1088
+ { status: 400 }
1089
+ );
1090
+ }
1091
+
1092
+ await deleteUserData(userId);
1093
+
1094
+ return NextResponse.json({ success: true, message: 'Account deleted successfully' });
1095
+ }
1096
+ `;
1097
+ }
1098
+ /**
1099
+ * Generates the admin settings API route (GET, PUT)
1100
+ * GET: Lists all system settings
1101
+ * PUT: Updates system settings with upsert
1102
+ * @returns TypeScript content for app/api/admin/settings/route.ts
1103
+ */
1104
+ function generateAdminSettingsRoute() {
1105
+ return `// @chimerai component=AdminSettingsAPI version=1.1
1106
+ import { NextRequest, NextResponse } from 'next/server';
1107
+ import { prisma } from '@/lib/prisma';
1108
+ import { requirePermission } from '@/lib/auth/require-permission';
1109
+ import { logAuditAction } from '@/lib/audit';
1110
+ import { getServerSession } from 'next-auth';
1111
+ import { authOptions } from '@/lib/auth';
1112
+
1113
+ const ALLOWED_KEYS = ['app.name', 'app.language', 'app.maintenanceMode'];
1114
+
1115
+ // GET /api/admin/settings — fetch all settings
1116
+ export async function GET(req: NextRequest) {
1117
+ const permissionError = await requirePermission('settings:read');
1118
+ if (permissionError) return permissionError;
1119
+
1120
+ try {
1121
+ const settings = await prisma.systemSetting.findMany();
1122
+
1123
+ const settingsMap: Record<string, string> = {};
1124
+ for (const s of settings) {
1125
+ settingsMap[s.key] = s.value;
1126
+ }
1127
+
1128
+ const result = {
1129
+ 'app.name': settingsMap['app.name'] || 'ChimerAI',
1130
+ 'app.language': settingsMap['app.language'] || 'en',
1131
+ 'app.maintenanceMode': settingsMap['app.maintenanceMode'] || 'false',
1132
+ };
1133
+
1134
+ return NextResponse.json(result);
1135
+ } catch (error) {
1136
+ console.error('Failed to fetch settings:', error);
1137
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
1138
+ }
1139
+ }
1140
+
1141
+ // PUT /api/admin/settings — update settings
1142
+ export async function PUT(req: NextRequest) {
1143
+ const permissionError = await requirePermission('settings:write');
1144
+ if (permissionError) return permissionError;
1145
+
1146
+ try {
1147
+ const session = await getServerSession(authOptions);
1148
+ const body = await req.json();
1149
+
1150
+ const updates: { key: string; value: string }[] = [];
1151
+
1152
+ for (const [key, value] of Object.entries(body)) {
1153
+ if (ALLOWED_KEYS.includes(key) && typeof value === 'string') {
1154
+ updates.push({ key, value });
1155
+ }
1156
+ }
1157
+
1158
+ if (updates.length === 0) {
1159
+ return NextResponse.json({ error: 'No valid settings provided' }, { status: 400 });
1160
+ }
1161
+
1162
+ for (const { key, value } of updates) {
1163
+ await prisma.systemSetting.upsert({
1164
+ where: { key },
1165
+ update: { value },
1166
+ create: { key, value },
1167
+ });
1168
+ }
1169
+
1170
+ await logAuditAction({
1171
+ action: 'settings.update',
1172
+ userId: session?.user?.id || 'unknown',
1173
+ metadata: { keys: updates.map((u) => u.key) },
1174
+ });
1175
+
1176
+ return NextResponse.json({ success: true, updated: updates.map((u) => u.key) });
1177
+ } catch (error) {
1178
+ console.error('Failed to update settings:', error);
1179
+ return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
1180
+ }
1181
+ }
1182
+ `;
1183
+ }
1184
+ /**
1185
+ * Generates the public app settings API route (GET)
1186
+ * Public endpoint — no auth needed (reads app name and language)
1187
+ * @returns TypeScript content for app/api/app-settings/route.ts
1188
+ */
1189
+ function generateAppSettingsRoute() {
1190
+ return `// @chimerai component=AppSettingsAPI version=1.1
1191
+ import { NextResponse } from 'next/server';
1192
+ import { prisma } from '@/lib/prisma';
1193
+
1194
+ // Force dynamic rendering — Next.js caches static GET routes by default,
1195
+ // which would serve stale app-name values after admin changes.
1196
+ export const dynamic = 'force-dynamic';
1197
+
1198
+ // Public endpoint — no auth needed (just reads app name)
1199
+ export async function GET() {
1200
+ try {
1201
+ const settings = await prisma.systemSetting.findMany({
1202
+ where: { key: { in: ['app.name', 'app.language'] } },
1203
+ });
1204
+
1205
+ const map: Record<string, string> = {};
1206
+ for (const s of settings) {
1207
+ map[s.key] = s.value;
1208
+ }
1209
+
1210
+ return NextResponse.json({
1211
+ appName: map['app.name'] || 'ChimerAI',
1212
+ language: map['app.language'] || 'en',
1213
+ });
1214
+ } catch {
1215
+ return NextResponse.json({ appName: 'ChimerAI', language: 'en' });
1216
+ }
1217
+ }
1218
+ `;
1219
+ }