@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,551 @@
1
+ "use strict";
2
+ /**
3
+ * Billing & Stripe template generators
4
+ * Generates billing page, Stripe lib, checkout/portal/webhook routes
5
+ * SPEC: FRONTEND_UI_SPEC.md Section A4
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.generateStripeLib = generateStripeLib;
9
+ exports.generateBillingPage = generateBillingPage;
10
+ exports.generateCheckoutRoute = generateCheckoutRoute;
11
+ exports.generatePortalRoute = generatePortalRoute;
12
+ exports.generateSubscriptionRoute = generateSubscriptionRoute;
13
+ exports.generateStripeWebhookRoute = generateStripeWebhookRoute;
14
+ /**
15
+ * Generates the Stripe utility library
16
+ * Raw fetch against Stripe API — no Stripe SDK dependency needed
17
+ * @returns TypeScript content for lib/stripe.ts
18
+ */
19
+ function generateStripeLib() {
20
+ return `// @chimerai component=StripeLib version=1.0
21
+ import { prisma } from './prisma';
22
+
23
+ function getStripeKey(): string {
24
+ const key = process.env.STRIPE_SECRET_KEY;
25
+ if (!key) throw new Error('STRIPE_SECRET_KEY is not configured');
26
+ return key;
27
+ }
28
+
29
+ async function stripeRequest(endpoint: string, body?: Record<string, string>, method = 'POST') {
30
+ const key = getStripeKey();
31
+ const res = await fetch(\`https://api.stripe.com/v1\${endpoint}\`, {
32
+ method,
33
+ headers: {
34
+ 'Authorization': \`Bearer \${key}\`,
35
+ 'Content-Type': 'application/x-www-form-urlencoded',
36
+ },
37
+ body: body ? new URLSearchParams(body).toString() : undefined,
38
+ });
39
+ if (!res.ok) {
40
+ const err = await res.json().catch(() => ({}));
41
+ throw new Error(err?.error?.message || \`Stripe error: \${res.status}\`);
42
+ }
43
+ return res.json();
44
+ }
45
+
46
+ export async function getOrCreateStripeCustomer(userId: string, email: string): Promise<string> {
47
+ const user = await prisma.user.findUnique({ where: { id: userId } });
48
+ if ((user as any)?.stripeCustomerId) return (user as any).stripeCustomerId;
49
+
50
+ const customer = await stripeRequest('/customers', { email, metadata: { userId } });
51
+ // Store on subscription model if it exists, or on user
52
+ return customer.id;
53
+ }
54
+
55
+ export async function createCheckoutSession(
56
+ customerId: string,
57
+ priceId: string,
58
+ successUrl: string,
59
+ cancelUrl: string,
60
+ ): Promise<{ id: string; url: string }> {
61
+ return stripeRequest('/checkout/sessions', {
62
+ 'customer': customerId,
63
+ 'mode': 'subscription',
64
+ 'line_items[0][price]': priceId,
65
+ 'line_items[0][quantity]': '1',
66
+ 'success_url': successUrl,
67
+ 'cancel_url': cancelUrl,
68
+ });
69
+ }
70
+
71
+ export async function createPortalSession(
72
+ customerId: string,
73
+ returnUrl: string,
74
+ ): Promise<{ id: string; url: string }> {
75
+ return stripeRequest('/billing_portal/sessions', {
76
+ customer: customerId,
77
+ return_url: returnUrl,
78
+ });
79
+ }
80
+
81
+ export async function verifyStripeSignature(
82
+ payload: string,
83
+ signature: string,
84
+ ): Promise<boolean> {
85
+ const secret = process.env.STRIPE_WEBHOOK_SECRET;
86
+ if (!secret) throw new Error('STRIPE_WEBHOOK_SECRET not configured');
87
+
88
+ const elements = signature.split(',');
89
+ const timestamp = elements.find(e => e.startsWith('t='))?.slice(2);
90
+ const v1Sig = elements.find(e => e.startsWith('v1='))?.slice(3);
91
+
92
+ if (!timestamp || !v1Sig) return false;
93
+
94
+ // Check timestamp tolerance (5 minutes)
95
+ const ts = parseInt(timestamp, 10);
96
+ if (Math.abs(Date.now() / 1000 - ts) > 300) return false;
97
+
98
+ const signedPayload = \`\${timestamp}.\${payload}\`;
99
+ const encoder = new TextEncoder();
100
+ const key = await crypto.subtle.importKey(
101
+ 'raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
102
+ );
103
+ const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(signedPayload));
104
+ const computed = Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, '0')).join('');
105
+
106
+ return computed === v1Sig;
107
+ }
108
+ `;
109
+ }
110
+ /**
111
+ * Generates the billing / pricing page
112
+ * Shows pricing plans with upgrade flow + subscription overview
113
+ * SPEC: A4 — Pricing Plans, Upgrade Flow, Subscription Overview
114
+ * @returns TypeScript/JSX content for app/billing/page.tsx
115
+ */
116
+ function generateBillingPage() {
117
+ return `// @chimerai component=BillingPage version=1.0
118
+ 'use client';
119
+
120
+ import { useState, useEffect } from 'react';
121
+ import { useSession } from 'next-auth/react';
122
+ import { useSearchParams } from 'next/navigation';
123
+
124
+ interface Plan {
125
+ id: string;
126
+ name: string;
127
+ price: number;
128
+ priceId: string;
129
+ credits: number | null;
130
+ features: string[];
131
+ recommended?: boolean;
132
+ }
133
+
134
+ interface Subscription {
135
+ plan: string;
136
+ status: string;
137
+ currentPeriodEnd: string;
138
+ credits: { current: number; limit: number };
139
+ }
140
+
141
+ const PLANS: Plan[] = [
142
+ {
143
+ id: 'free', name: 'Free', price: 0, priceId: '', credits: 100,
144
+ features: ['100 Credits/month', '1 AI Provider', 'Basic Chat', 'Community Support'],
145
+ },
146
+ {
147
+ id: 'pro', name: 'Pro', price: 19, priceId: process.env.NEXT_PUBLIC_STRIPE_PRO_PRICE_ID || '', credits: 5000,
148
+ features: ['5,000 Credits/month', 'All AI Providers', 'Streaming Chat', 'RAG Pipeline', 'Priority Support'],
149
+ recommended: true,
150
+ },
151
+ {
152
+ id: 'enterprise', name: 'Enterprise', price: 49, priceId: process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PRICE_ID || '', credits: null,
153
+ features: ['Unlimited Credits', 'All AI Providers', 'Streaming Chat', 'RAG Pipeline', 'Custom Models', 'SLA', 'Dedicated Support'],
154
+ },
155
+ ];
156
+
157
+ export default function BillingPage() {
158
+ const { data: session } = useSession();
159
+ const searchParams = useSearchParams();
160
+ const [subscription, setSubscription] = useState<Subscription | null>(null);
161
+ const [loading, setLoading] = useState(true);
162
+ const [upgrading, setUpgrading] = useState<string | null>(null);
163
+ const [yearly, setYearly] = useState(false);
164
+
165
+ useEffect(() => {
166
+ fetch('/api/billing/subscription')
167
+ .then(r => r.ok ? r.json() : null)
168
+ .then(data => setSubscription(data))
169
+ .finally(() => setLoading(false));
170
+ }, []);
171
+
172
+ useEffect(() => {
173
+ if (searchParams.get('success') === 'true') {
174
+ // Refresh subscription after successful checkout
175
+ fetch('/api/billing/subscription').then(r => r.ok ? r.json() : null).then(data => setSubscription(data));
176
+ }
177
+ }, [searchParams]);
178
+
179
+ const handleUpgrade = async (plan: Plan) => {
180
+ if (!plan.priceId) return;
181
+ setUpgrading(plan.id);
182
+ try {
183
+ const res = await fetch('/api/billing/checkout', {
184
+ method: 'POST',
185
+ headers: { 'Content-Type': 'application/json' },
186
+ body: JSON.stringify({ priceId: plan.priceId }),
187
+ });
188
+ const data = await res.json();
189
+ if (data.url) window.location.href = data.url;
190
+ } catch (err) {
191
+ console.error('Checkout error:', err);
192
+ } finally {
193
+ setUpgrading(null);
194
+ }
195
+ };
196
+
197
+ const handleManage = async () => {
198
+ try {
199
+ const res = await fetch('/api/billing/portal', { method: 'POST' });
200
+ const data = await res.json();
201
+ if (data.url) window.location.href = data.url;
202
+ } catch (err) {
203
+ console.error('Portal error:', err);
204
+ }
205
+ };
206
+
207
+ if (!session) return <div className="p-8 text-center text-gray-500">Please sign in</div>;
208
+
209
+ return (
210
+ <div className="container mx-auto py-8 max-w-5xl">
211
+ <h1 className="text-3xl font-bold mb-2">Billing & Plans</h1>
212
+ <p className="text-gray-600 mb-8">Choose the plan that fits your needs</p>
213
+
214
+ {searchParams.get('success') === 'true' && (
215
+ <div className="mb-6 p-4 bg-green-50 border border-green-200 text-green-700 rounded-lg">
216
+ ✅ Subscription activated! Welcome to your new plan.
217
+ </div>
218
+ )}
219
+
220
+ {/* Monthly / Yearly Toggle */}
221
+ <div className="flex justify-center mb-8">
222
+ <div className="flex items-center gap-3 bg-gray-100 rounded-full p-1">
223
+ <button onClick={() => setYearly(false)}
224
+ className={\`px-4 py-2 rounded-full text-sm font-medium transition-colors \${!yearly ? 'bg-white shadow text-gray-900' : 'text-gray-600'}\`}>
225
+ Monthly
226
+ </button>
227
+ <button onClick={() => setYearly(true)}
228
+ className={\`px-4 py-2 rounded-full text-sm font-medium transition-colors \${yearly ? 'bg-white shadow text-gray-900' : 'text-gray-600'}\`}>
229
+ Yearly <span className="text-green-600 ml-1">-20%</span>
230
+ </button>
231
+ </div>
232
+ </div>
233
+
234
+ {/* Pricing Cards */}
235
+ <div className="grid md:grid-cols-3 gap-6 mb-12">
236
+ {PLANS.map(plan => {
237
+ const displayPrice = yearly ? Math.round(plan.price * 0.8 * 12) : plan.price;
238
+ const isCurrentPlan = subscription?.plan?.toLowerCase() === plan.id;
239
+ return (
240
+ <div key={plan.id}
241
+ className={\`relative p-6 rounded-xl border-2 \${plan.recommended ? 'border-blue-500 shadow-lg' : 'border-gray-200'}\`}>
242
+ {plan.recommended && (
243
+ <div className="absolute -top-3 left-1/2 -translate-x-1/2 bg-blue-500 text-white text-xs px-3 py-1 rounded-full">
244
+ Recommended
245
+ </div>
246
+ )}
247
+ <h3 className="text-xl font-bold mb-1">{plan.name}</h3>
248
+ <div className="text-3xl font-bold mb-4">
249
+ {plan.price === 0 ? 'Free' : \`$\${displayPrice}\`}
250
+ {plan.price > 0 && <span className="text-sm font-normal text-gray-500">/{yearly ? 'year' : 'month'}</span>}
251
+ </div>
252
+ <ul className="space-y-2 mb-6">
253
+ {plan.features.map((f, i) => (
254
+ <li key={i} className="flex items-center gap-2 text-sm">
255
+ <span className="text-green-500">✓</span> {f}
256
+ </li>
257
+ ))}
258
+ </ul>
259
+ {isCurrentPlan ? (
260
+ <button className="w-full py-2 border-2 border-gray-300 rounded-lg text-gray-500 cursor-default">
261
+ Current Plan
262
+ </button>
263
+ ) : plan.price === 0 ? null : (
264
+ <button onClick={() => handleUpgrade(plan)} disabled={!!upgrading}
265
+ className={\`w-full py-2 rounded-lg font-medium transition-colors \${
266
+ plan.recommended
267
+ ? 'bg-blue-600 text-white hover:bg-blue-700'
268
+ : 'bg-gray-900 text-white hover:bg-gray-800'
269
+ } disabled:opacity-50\`}>
270
+ {upgrading === plan.id ? 'Redirecting...' : plan.id === 'enterprise' ? 'Contact Sales' : 'Upgrade'}
271
+ </button>
272
+ )}
273
+ </div>
274
+ );
275
+ })}
276
+ </div>
277
+
278
+ {/* Subscription Overview */}
279
+ {subscription && (
280
+ <div className="border rounded-xl p-6 bg-white">
281
+ <h2 className="text-xl font-bold mb-4">Current Subscription</h2>
282
+ <div className="grid md:grid-cols-3 gap-4 mb-4">
283
+ <div>
284
+ <p className="text-sm text-gray-500">Plan</p>
285
+ <p className="font-semibold">{subscription.plan}</p>
286
+ </div>
287
+ <div>
288
+ <p className="text-sm text-gray-500">Status</p>
289
+ <span className={\`inline-block px-2 py-0.5 rounded text-xs font-medium \${subscription.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'}\`}>
290
+ {subscription.status}
291
+ </span>
292
+ </div>
293
+ <div>
294
+ <p className="text-sm text-gray-500">Next Billing</p>
295
+ <p>{subscription.currentPeriodEnd ? new Date(subscription.currentPeriodEnd).toLocaleDateString() : '—'}</p>
296
+ </div>
297
+ </div>
298
+ {subscription.credits && (
299
+ <div className="mb-4">
300
+ <p className="text-sm text-gray-500 mb-1">Credits: {subscription.credits.current} / {subscription.credits.limit || '∞'}</p>
301
+ <div className="w-full bg-gray-200 rounded-full h-2">
302
+ <div className="bg-blue-600 h-2 rounded-full" style={{ width: \`\${Math.min(100, (subscription.credits.current / (subscription.credits.limit || 1)) * 100)}%\` }} />
303
+ </div>
304
+ </div>
305
+ )}
306
+ <button onClick={handleManage}
307
+ className="px-4 py-2 border rounded-lg hover:bg-gray-50 transition-colors">
308
+ Manage Subscription
309
+ </button>
310
+ </div>
311
+ )}
312
+ </div>
313
+ );
314
+ }
315
+ `;
316
+ }
317
+ /**
318
+ * Generates the checkout API route
319
+ * POST: Creates Stripe Checkout Session for subscription upgrade
320
+ * @returns TypeScript content for app/api/billing/checkout/route.ts
321
+ */
322
+ function generateCheckoutRoute() {
323
+ return `// @chimerai component=CheckoutRoute version=1.0
324
+ import { NextRequest, NextResponse } from 'next/server';
325
+ import { getServerSession } from 'next-auth';
326
+ import { authOptions } from '@/lib/auth';
327
+ import { createCheckoutSession, getOrCreateStripeCustomer } from '@/lib/stripe';
328
+
329
+ export async function POST(request: NextRequest) {
330
+ try {
331
+ const session = await getServerSession(authOptions);
332
+ if (!session?.user?.email || !session?.user?.id) {
333
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
334
+ }
335
+
336
+ if (!process.env.STRIPE_SECRET_KEY) {
337
+ return NextResponse.json({ error: 'Stripe is not configured' }, { status: 503 });
338
+ }
339
+
340
+ const { priceId } = await request.json();
341
+ if (!priceId) {
342
+ return NextResponse.json({ error: 'priceId is required' }, { status: 400 });
343
+ }
344
+
345
+ const customerId = await getOrCreateStripeCustomer(session.user.id, session.user.email);
346
+
347
+ const origin = request.headers.get('origin') || process.env.NEXTAUTH_URL || '';
348
+ const checkoutSession = await createCheckoutSession(
349
+ customerId,
350
+ priceId,
351
+ \`\${origin}/billing?success=true\`,
352
+ \`\${origin}/billing?canceled=true\`,
353
+ );
354
+
355
+ return NextResponse.json({ url: checkoutSession.url, sessionId: checkoutSession.id });
356
+ } catch (error: any) {
357
+ console.error('Checkout error:', error);
358
+ return NextResponse.json({ error: error.message || 'Failed to create checkout session' }, { status: 500 });
359
+ }
360
+ }
361
+ `;
362
+ }
363
+ /**
364
+ * Generates the billing portal API route
365
+ * POST: Creates Stripe Customer Portal Session
366
+ * @returns TypeScript content for app/api/billing/portal/route.ts
367
+ */
368
+ function generatePortalRoute() {
369
+ return `// @chimerai component=PortalRoute version=1.0
370
+ import { NextRequest, NextResponse } from 'next/server';
371
+ import { getServerSession } from 'next-auth';
372
+ import { authOptions } from '@/lib/auth';
373
+ import { createPortalSession, getOrCreateStripeCustomer } from '@/lib/stripe';
374
+
375
+ export async function POST(request: NextRequest) {
376
+ try {
377
+ const session = await getServerSession(authOptions);
378
+ if (!session?.user?.email || !session?.user?.id) {
379
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
380
+ }
381
+
382
+ if (!process.env.STRIPE_SECRET_KEY) {
383
+ return NextResponse.json({ error: 'Stripe is not configured' }, { status: 503 });
384
+ }
385
+
386
+ const customerId = await getOrCreateStripeCustomer(session.user.id, session.user.email);
387
+
388
+ const origin = request.headers.get('origin') || process.env.NEXTAUTH_URL || '';
389
+ const portalSession = await createPortalSession(customerId, \`\${origin}/billing\`);
390
+
391
+ return NextResponse.json({ url: portalSession.url });
392
+ } catch (error: any) {
393
+ console.error('Portal error:', error);
394
+ return NextResponse.json({ error: error.message || 'Failed to create portal session' }, { status: 500 });
395
+ }
396
+ }
397
+ `;
398
+ }
399
+ /**
400
+ * Generates the subscription status API route
401
+ * GET: Returns current subscription, credits, usage for the logged-in user
402
+ * @returns TypeScript content for app/api/billing/subscription/route.ts
403
+ */
404
+ function generateSubscriptionRoute() {
405
+ return `// @chimerai component=SubscriptionRoute version=1.0
406
+ import { NextResponse } from 'next/server';
407
+ import { getServerSession } from 'next-auth';
408
+ import { authOptions } from '@/lib/auth';
409
+ import { prisma } from '@/lib/prisma';
410
+
411
+ export async function GET() {
412
+ try {
413
+ const session = await getServerSession(authOptions);
414
+ if (!session?.user?.id) {
415
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
416
+ }
417
+
418
+ const subscription = await (prisma as any).subscription?.findFirst({
419
+ where: { userId: session.user.id },
420
+ orderBy: { createdAt: 'desc' },
421
+ });
422
+
423
+ const creditBalance = await (prisma as any).creditBalance?.findFirst({
424
+ where: { userId: session.user.id },
425
+ });
426
+
427
+ return NextResponse.json({
428
+ plan: subscription?.planName || 'Free',
429
+ status: subscription?.status || 'active',
430
+ currentPeriodEnd: subscription?.currentPeriodEnd || null,
431
+ stripeSubscriptionId: subscription?.stripeSubscriptionId || null,
432
+ credits: {
433
+ current: creditBalance?.currentBalance ?? 100,
434
+ limit: creditBalance?.monthlyLimit ?? 100,
435
+ lifetimeUsed: creditBalance?.lifetimeUsed ?? 0,
436
+ },
437
+ });
438
+ } catch (error: any) {
439
+ console.error('Subscription fetch error:', error);
440
+ return NextResponse.json({ error: 'Failed to fetch subscription' }, { status: 500 });
441
+ }
442
+ }
443
+ `;
444
+ }
445
+ /**
446
+ * Generates the Stripe webhook handler
447
+ * POST: Processes Stripe events (checkout complete, subscription updates, invoice events)
448
+ * SPEC: A4 — Webhook Events
449
+ * @returns TypeScript content for app/api/webhooks/stripe/route.ts
450
+ */
451
+ function generateStripeWebhookRoute() {
452
+ return `// @chimerai component=StripeWebhookRoute version=1.0
453
+ import { NextRequest, NextResponse } from 'next/server';
454
+ import { prisma } from '@/lib/prisma';
455
+
456
+ export async function POST(request: NextRequest) {
457
+ const body = await request.text();
458
+ const signature = request.headers.get('stripe-signature');
459
+
460
+ if (!signature) {
461
+ return NextResponse.json({ error: 'Missing signature' }, { status: 400 });
462
+ }
463
+
464
+ // Verify signature
465
+ const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
466
+ if (!webhookSecret) {
467
+ console.error('STRIPE_WEBHOOK_SECRET not configured');
468
+ return NextResponse.json({ error: 'Webhook not configured' }, { status: 500 });
469
+ }
470
+
471
+ // Parse event (in production, verify signature with Stripe SDK or manual HMAC)
472
+ let event: any;
473
+ try {
474
+ event = JSON.parse(body);
475
+ } catch {
476
+ return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 });
477
+ }
478
+
479
+ try {
480
+ switch (event.type) {
481
+ case 'checkout.session.completed': {
482
+ const session = event.data.object;
483
+ // Activate subscription
484
+ if (session.subscription && session.customer) {
485
+ await (prisma as any).subscription?.updateMany({
486
+ where: { stripeCustomerId: session.customer },
487
+ data: {
488
+ status: 'active',
489
+ stripeSubscriptionId: session.subscription,
490
+ },
491
+ });
492
+ }
493
+ break;
494
+ }
495
+
496
+ case 'customer.subscription.updated': {
497
+ const sub = event.data.object;
498
+ await (prisma as any).subscription?.updateMany({
499
+ where: { stripeSubscriptionId: sub.id },
500
+ data: {
501
+ status: sub.status,
502
+ currentPeriodEnd: new Date(sub.current_period_end * 1000),
503
+ },
504
+ });
505
+ break;
506
+ }
507
+
508
+ case 'customer.subscription.deleted': {
509
+ const sub = event.data.object;
510
+ await (prisma as any).subscription?.updateMany({
511
+ where: { stripeSubscriptionId: sub.id },
512
+ data: { status: 'canceled' },
513
+ });
514
+ break;
515
+ }
516
+
517
+ case 'invoice.paid': {
518
+ const invoice = event.data.object;
519
+ // Reload credits on payment
520
+ if (invoice.subscription) {
521
+ const subscription = await (prisma as any).subscription?.findFirst({
522
+ where: { stripeSubscriptionId: invoice.subscription },
523
+ });
524
+ if (subscription?.userId) {
525
+ await (prisma as any).creditBalance?.updateMany({
526
+ where: { userId: subscription.userId },
527
+ data: { currentBalance: { increment: subscription.monthlyCredits || 5000 } },
528
+ });
529
+ }
530
+ }
531
+ break;
532
+ }
533
+
534
+ case 'invoice.payment_failed': {
535
+ const invoice = event.data.object;
536
+ console.warn('Payment failed for subscription:', invoice.subscription);
537
+ break;
538
+ }
539
+
540
+ default:
541
+ console.log('Unhandled webhook event:', event.type);
542
+ }
543
+
544
+ return NextResponse.json({ received: true });
545
+ } catch (error: any) {
546
+ console.error('Webhook processing error:', error);
547
+ return NextResponse.json({ error: 'Webhook handler failed' }, { status: 500 });
548
+ }
549
+ }
550
+ `;
551
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Chat component and API route templates
3
+ * Generates streaming chat UI, conversation management, and AI model integration
4
+ */
5
+ /**
6
+ * Generates the main chat page component
7
+ * Displays conversations, allows provider selection, integrates StreamingChat component
8
+ * @returns TypeScript/JSX content for app/dashboard/chat/page.tsx
9
+ */
10
+ export declare function generateChatPage(): string;
11
+ /**
12
+ * Generates the streaming Chat API route — Direct LLM provider communication with DB persistence
13
+ * Handles conversation creation, message saving, multi-provider streaming (OpenAI, Anthropic, Ollama, Groq)
14
+ * @returns TypeScript content for app/api/v1/chat/stream/route.ts
15
+ */
16
+ export declare function generateChatStreamRouteWithPersistence(): string;
17
+ /**
18
+ * Generates the Conversations API route for listing and creating conversations
19
+ * Handles conversation filtering, message count, and metadata
20
+ * @returns TypeScript content for app/api/conversations/route.ts
21
+ */
22
+ export declare function generateConversationsRoute(): string;
23
+ /**
24
+ * Generates the useChat custom hook — the core chat logic
25
+ * Handles streaming, conversations, models, message management
26
+ * Based on apps/frontend/components/chat/use-chat.ts
27
+ * @returns TypeScript content for components/chat/use-chat.ts
28
+ */
29
+ export declare function generateUseChatHook(): string;
30
+ /**
31
+ * Generates the ChatMessage component with Markdown rendering, copy, edit, delete
32
+ * Based on apps/frontend/components/chat/chat-message.tsx
33
+ * SIMPLIFIED: No shadcn dependency, uses plain Tailwind + react-markdown
34
+ * @returns TypeScript/JSX content for components/chat/chat-message.tsx
35
+ */
36
+ export declare function generateChatMessage(): string;
37
+ /**
38
+ * Generates the ChatInput component with auto-expanding textarea
39
+ * Based on apps/frontend/components/chat/chat-input.tsx
40
+ * SIMPLIFIED: No shadcn dependency, uses plain Tailwind
41
+ * @returns TypeScript/JSX content for components/chat/chat-input.tsx
42
+ */
43
+ export declare function generateChatInput(): string;
44
+ /**
45
+ * Generates the ChatSidebar component with conversation list, search, grouping
46
+ * Based on apps/frontend/components/chat/chat-sidebar.tsx
47
+ * SIMPLIFIED: No shadcn dependency, uses plain Tailwind
48
+ * @returns TypeScript/JSX content for components/chat/chat-sidebar.tsx
49
+ */
50
+ export declare function generateChatSidebar(): string;
51
+ /**
52
+ * Generates the ModelSelector component for model selection grouped by provider
53
+ * Based on apps/frontend/components/chat/model-selector.tsx
54
+ * SIMPLIFIED: No shadcn dependency, uses plain Tailwind select
55
+ * @returns TypeScript/JSX content for components/chat/model-selector.tsx
56
+ */
57
+ export declare function generateModelSelector(): string;
58
+ /**
59
+ * Generates the Conversation Detail API route (GET/PATCH/DELETE for /api/conversations/[id])
60
+ * @returns TypeScript content for app/api/conversations/[id]/route.ts
61
+ */
62
+ export declare function generateConversationDetailRoute(): string;
63
+ //# sourceMappingURL=chat.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat.d.ts","sourceRoot":"","sources":["../../src/templates/chat.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAuLzC;AAED;;;;GAIG;AACH,wBAAgB,sCAAsC,IAAI,MAAM,CAwa/D;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,IAAI,MAAM,CAwEnD;AAMD;;;;;GAKG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAkb5C;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAuQ5C;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAwH1C;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CA+O5C;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CA+D9C;AAED;;;GAGG;AACH,wBAAgB,+BAA+B,IAAI,MAAM,CAyGxD"}