create-solostack 1.2.2 → 1.3.0

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.
@@ -0,0 +1,521 @@
1
+ /**
2
+ * Advanced Stripe Integration Generator - Pro Feature
3
+ * Generates subscription management, webhooks, and billing page
4
+ */
5
+
6
+ import path from 'path';
7
+ import { writeFile, ensureDir } from '../../utils/files.js';
8
+
9
+ export async function generateAdvancedStripe(projectPath) {
10
+ // Ensure directories
11
+ await ensureDir(path.join(projectPath, 'src/app/api/stripe/webhook'));
12
+ await ensureDir(path.join(projectPath, 'src/app/api/stripe/create-subscription'));
13
+ await ensureDir(path.join(projectPath, 'src/app/api/stripe/cancel-subscription'));
14
+ await ensureDir(path.join(projectPath, 'src/app/api/stripe/customer-portal'));
15
+ await ensureDir(path.join(projectPath, 'src/app/dashboard/billing'));
16
+
17
+ // Generate Stripe utility library
18
+ const stripeLib = `import Stripe from 'stripe';
19
+
20
+ if (!process.env.STRIPE_SECRET_KEY) {
21
+ throw new Error('STRIPE_SECRET_KEY is not set');
22
+ }
23
+
24
+ export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
25
+ apiVersion: '2024-11-20.acacia',
26
+ typescript: true,
27
+ });
28
+
29
+ /**
30
+ * Pricing Plans Configuration
31
+ */
32
+ export const PLANS = {
33
+ pro: {
34
+ name: 'Pro',
35
+ priceId: process.env.STRIPE_PRO_PRICE_ID || 'price_pro',
36
+ price: 29,
37
+ features: [
38
+ 'Unlimited projects',
39
+ 'Priority support',
40
+ 'API access',
41
+ 'Advanced analytics',
42
+ ],
43
+ },
44
+ enterprise: {
45
+ name: 'Enterprise',
46
+ priceId: process.env.STRIPE_ENTERPRISE_PRICE_ID || 'price_enterprise',
47
+ price: 99,
48
+ features: [
49
+ 'Everything in Pro',
50
+ 'Custom integrations',
51
+ 'Dedicated account manager',
52
+ 'SLA guarantee',
53
+ ],
54
+ },
55
+ };
56
+
57
+ /**
58
+ * Get or create Stripe customer for user
59
+ */
60
+ export async function getOrCreateCustomer(userId: string, email: string) {
61
+ const { db } = await import('@/lib/db');
62
+
63
+ const user = await db.user.findUnique({
64
+ where: { id: userId },
65
+ select: { stripeCustomerId: true },
66
+ });
67
+
68
+ if (user?.stripeCustomerId) {
69
+ return user.stripeCustomerId;
70
+ }
71
+
72
+ const customer = await stripe.customers.create({
73
+ email,
74
+ metadata: { userId },
75
+ });
76
+
77
+ await db.user.update({
78
+ where: { id: userId },
79
+ data: { stripeCustomerId: customer.id },
80
+ });
81
+
82
+ return customer.id;
83
+ }
84
+ `;
85
+
86
+ await writeFile(path.join(projectPath, 'src/lib/stripe.ts'), stripeLib);
87
+
88
+ // Generate webhook handler
89
+ const webhookHandler = `import { NextRequest, NextResponse } from 'next/server';
90
+ import { stripe } from '@/lib/stripe';
91
+ import Stripe from 'stripe';
92
+
93
+ const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!;
94
+
95
+ export async function POST(req: NextRequest) {
96
+ const body = await req.text();
97
+ const signature = req.headers.get('stripe-signature')!;
98
+
99
+ let event: Stripe.Event;
100
+
101
+ try {
102
+ event = stripe.webhooks.constructEvent(body, signature, webhookSecret);
103
+ } catch (err: any) {
104
+ console.error('Webhook signature verification failed:', err.message);
105
+ return NextResponse.json({ error: 'Invalid signature' }, { status: 400 });
106
+ }
107
+
108
+ const { db } = await import('@/lib/db');
109
+
110
+ // Check for idempotency
111
+ const existingEvent = await db.stripeEvent.findUnique({
112
+ where: { id: event.id },
113
+ });
114
+
115
+ if (existingEvent?.processed) {
116
+ return NextResponse.json({ received: true, duplicate: true });
117
+ }
118
+
119
+ // Record the event
120
+ await db.stripeEvent.upsert({
121
+ where: { id: event.id },
122
+ update: {},
123
+ create: { id: event.id, type: event.type },
124
+ });
125
+
126
+ try {
127
+ switch (event.type) {
128
+ case 'customer.subscription.created':
129
+ case 'customer.subscription.updated': {
130
+ const subscription = event.data.object as Stripe.Subscription;
131
+ await handleSubscriptionChange(subscription, db);
132
+ break;
133
+ }
134
+
135
+ case 'customer.subscription.deleted': {
136
+ const subscription = event.data.object as Stripe.Subscription;
137
+ await handleSubscriptionDeleted(subscription, db);
138
+ break;
139
+ }
140
+
141
+ case 'invoice.payment_succeeded': {
142
+ const invoice = event.data.object as Stripe.Invoice;
143
+ await handlePaymentSucceeded(invoice, db);
144
+ break;
145
+ }
146
+
147
+ case 'invoice.payment_failed': {
148
+ const invoice = event.data.object as Stripe.Invoice;
149
+ await handlePaymentFailed(invoice, db);
150
+ break;
151
+ }
152
+ }
153
+
154
+ // Mark event as processed
155
+ await db.stripeEvent.update({
156
+ where: { id: event.id },
157
+ data: { processed: true },
158
+ });
159
+
160
+ } catch (error) {
161
+ console.error('Error processing webhook:', error);
162
+ return NextResponse.json({ error: 'Webhook handler failed' }, { status: 500 });
163
+ }
164
+
165
+ return NextResponse.json({ received: true });
166
+ }
167
+
168
+ async function handleSubscriptionChange(subscription: Stripe.Subscription, db: any) {
169
+ const customerId = subscription.customer as string;
170
+ const user = await db.user.findFirst({
171
+ where: { stripeCustomerId: customerId },
172
+ });
173
+
174
+ if (!user) return;
175
+
176
+ const status = mapSubscriptionStatus(subscription.status);
177
+
178
+ await db.subscription.upsert({
179
+ where: { userId: user.id },
180
+ update: {
181
+ stripeSubscriptionId: subscription.id,
182
+ stripePriceId: subscription.items.data[0].price.id,
183
+ status,
184
+ currentPeriodStart: new Date(subscription.current_period_start * 1000),
185
+ currentPeriodEnd: new Date(subscription.current_period_end * 1000),
186
+ cancelAtPeriodEnd: subscription.cancel_at_period_end,
187
+ },
188
+ create: {
189
+ userId: user.id,
190
+ stripeSubscriptionId: subscription.id,
191
+ stripePriceId: subscription.items.data[0].price.id,
192
+ status,
193
+ currentPeriodStart: new Date(subscription.current_period_start * 1000),
194
+ currentPeriodEnd: new Date(subscription.current_period_end * 1000),
195
+ cancelAtPeriodEnd: subscription.cancel_at_period_end,
196
+ },
197
+ });
198
+ }
199
+
200
+ async function handleSubscriptionDeleted(subscription: Stripe.Subscription, db: any) {
201
+ await db.subscription.delete({
202
+ where: { stripeSubscriptionId: subscription.id },
203
+ }).catch(() => {}); // Ignore if not found
204
+ }
205
+
206
+ async function handlePaymentSucceeded(invoice: Stripe.Invoice, db: any) {
207
+ if (!invoice.customer) return;
208
+
209
+ const user = await db.user.findFirst({
210
+ where: { stripeCustomerId: invoice.customer as string },
211
+ });
212
+
213
+ if (!user) return;
214
+
215
+ await db.payment.create({
216
+ data: {
217
+ userId: user.id,
218
+ stripePaymentId: invoice.payment_intent as string,
219
+ amount: invoice.amount_paid,
220
+ currency: invoice.currency,
221
+ status: 'succeeded',
222
+ },
223
+ });
224
+ }
225
+
226
+ async function handlePaymentFailed(invoice: Stripe.Invoice, db: any) {
227
+ // Send email notification about failed payment
228
+ // await sendPaymentFailedEmail(user.email);
229
+ console.log('Payment failed for invoice:', invoice.id);
230
+ }
231
+
232
+ function mapSubscriptionStatus(status: string) {
233
+ const statusMap: Record<string, string> = {
234
+ active: 'ACTIVE',
235
+ canceled: 'CANCELED',
236
+ past_due: 'PAST_DUE',
237
+ trialing: 'TRIALING',
238
+ incomplete: 'INCOMPLETE',
239
+ };
240
+ return statusMap[status] || 'INCOMPLETE';
241
+ }
242
+ `;
243
+
244
+ await writeFile(
245
+ path.join(projectPath, 'src/app/api/stripe/webhook/route.ts'),
246
+ webhookHandler
247
+ );
248
+
249
+ // Generate create subscription endpoint
250
+ const createSubscription = `import { NextRequest, NextResponse } from 'next/server';
251
+ import { auth } from '@/lib/auth';
252
+ import { stripe, getOrCreateCustomer, PLANS } from '@/lib/stripe';
253
+
254
+ export async function POST(req: NextRequest) {
255
+ try {
256
+ const session = await auth();
257
+
258
+ if (!session?.user?.id) {
259
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
260
+ }
261
+
262
+ const { priceId } = await req.json();
263
+
264
+ if (!priceId) {
265
+ return NextResponse.json({ error: 'Price ID required' }, { status: 400 });
266
+ }
267
+
268
+ const customerId = await getOrCreateCustomer(session.user.id, session.user.email!);
269
+
270
+ const checkoutSession = await stripe.checkout.sessions.create({
271
+ customer: customerId,
272
+ mode: 'subscription',
273
+ payment_method_types: ['card'],
274
+ line_items: [
275
+ {
276
+ price: priceId,
277
+ quantity: 1,
278
+ },
279
+ ],
280
+ success_url: \`\${process.env.NEXTAUTH_URL}/dashboard/billing?success=true\`,
281
+ cancel_url: \`\${process.env.NEXTAUTH_URL}/dashboard/billing?canceled=true\`,
282
+ metadata: {
283
+ userId: session.user.id,
284
+ },
285
+ });
286
+
287
+ return NextResponse.json({ url: checkoutSession.url });
288
+ } catch (error: any) {
289
+ console.error('Create subscription error:', error);
290
+ return NextResponse.json({ error: error.message }, { status: 500 });
291
+ }
292
+ }
293
+ `;
294
+
295
+ await writeFile(
296
+ path.join(projectPath, 'src/app/api/stripe/create-subscription/route.ts'),
297
+ createSubscription
298
+ );
299
+
300
+ // Generate customer portal endpoint
301
+ const customerPortal = `import { NextRequest, NextResponse } from 'next/server';
302
+ import { auth } from '@/lib/auth';
303
+ import { stripe } from '@/lib/stripe';
304
+ import { db } from '@/lib/db';
305
+
306
+ export async function POST(req: NextRequest) {
307
+ try {
308
+ const session = await auth();
309
+
310
+ if (!session?.user?.id) {
311
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
312
+ }
313
+
314
+ const user = await db.user.findUnique({
315
+ where: { id: session.user.id },
316
+ select: { stripeCustomerId: true },
317
+ });
318
+
319
+ if (!user?.stripeCustomerId) {
320
+ return NextResponse.json({ error: 'No billing account found' }, { status: 400 });
321
+ }
322
+
323
+ const portalSession = await stripe.billingPortal.sessions.create({
324
+ customer: user.stripeCustomerId,
325
+ return_url: \`\${process.env.NEXTAUTH_URL}/dashboard/billing\`,
326
+ });
327
+
328
+ return NextResponse.json({ url: portalSession.url });
329
+ } catch (error: any) {
330
+ console.error('Customer portal error:', error);
331
+ return NextResponse.json({ error: error.message }, { status: 500 });
332
+ }
333
+ }
334
+ `;
335
+
336
+ await writeFile(
337
+ path.join(projectPath, 'src/app/api/stripe/customer-portal/route.ts'),
338
+ customerPortal
339
+ );
340
+
341
+ // Generate billing page
342
+ const billingPage = `'use client';
343
+
344
+ import { useState, useEffect } from 'react';
345
+ import { useSearchParams } from 'next/navigation';
346
+ import { CreditCard, Check, Loader2, ExternalLink } from 'lucide-react';
347
+
348
+ interface Subscription {
349
+ status: string;
350
+ stripePriceId: string;
351
+ currentPeriodEnd: string;
352
+ cancelAtPeriodEnd: boolean;
353
+ }
354
+
355
+ export default function BillingPage() {
356
+ const [subscription, setSubscription] = useState<Subscription | null>(null);
357
+ const [loading, setLoading] = useState(true);
358
+ const [actionLoading, setActionLoading] = useState(false);
359
+ const searchParams = useSearchParams();
360
+ const success = searchParams.get('success');
361
+ const canceled = searchParams.get('canceled');
362
+
363
+ useEffect(() => {
364
+ fetchSubscription();
365
+ }, []);
366
+
367
+ async function fetchSubscription() {
368
+ try {
369
+ const res = await fetch('/api/user/subscription');
370
+ if (res.ok) {
371
+ const data = await res.json();
372
+ setSubscription(data.subscription);
373
+ }
374
+ } finally {
375
+ setLoading(false);
376
+ }
377
+ }
378
+
379
+ async function handleSubscribe(priceId: string) {
380
+ setActionLoading(true);
381
+ try {
382
+ const res = await fetch('/api/stripe/create-subscription', {
383
+ method: 'POST',
384
+ headers: { 'Content-Type': 'application/json' },
385
+ body: JSON.stringify({ priceId }),
386
+ });
387
+ const data = await res.json();
388
+ if (data.url) {
389
+ window.location.href = data.url;
390
+ }
391
+ } finally {
392
+ setActionLoading(false);
393
+ }
394
+ }
395
+
396
+ async function handleManageBilling() {
397
+ setActionLoading(true);
398
+ try {
399
+ const res = await fetch('/api/stripe/customer-portal', {
400
+ method: 'POST',
401
+ });
402
+ const data = await res.json();
403
+ if (data.url) {
404
+ window.location.href = data.url;
405
+ }
406
+ } finally {
407
+ setActionLoading(false);
408
+ }
409
+ }
410
+
411
+ if (loading) {
412
+ return (
413
+ <div className="flex items-center justify-center min-h-[400px]">
414
+ <Loader2 className="h-8 w-8 animate-spin text-indigo-600" />
415
+ </div>
416
+ );
417
+ }
418
+
419
+ return (
420
+ <div className="max-w-4xl mx-auto p-6">
421
+ <h1 className="text-3xl font-bold mb-2">Billing</h1>
422
+ <p className="text-gray-600 mb-8">Manage your subscription and billing</p>
423
+
424
+ {success && (
425
+ <div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-6">
426
+ <p className="text-green-800">🎉 Subscription activated successfully!</p>
427
+ </div>
428
+ )}
429
+
430
+ {canceled && (
431
+ <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-6">
432
+ <p className="text-yellow-800">Checkout was canceled. Your subscription was not changed.</p>
433
+ </div>
434
+ )}
435
+
436
+ {subscription ? (
437
+ <div className="bg-white rounded-xl border p-6 mb-8">
438
+ <div className="flex items-center justify-between mb-4">
439
+ <div>
440
+ <h2 className="text-xl font-semibold">Current Plan</h2>
441
+ <p className="text-gray-600">
442
+ Status: <span className="font-medium text-green-600">{subscription.status}</span>
443
+ </p>
444
+ </div>
445
+ <CreditCard className="h-8 w-8 text-indigo-600" />
446
+ </div>
447
+
448
+ <p className="text-sm text-gray-500 mb-4">
449
+ Next billing date: {new Date(subscription.currentPeriodEnd).toLocaleDateString()}
450
+ {subscription.cancelAtPeriodEnd && (
451
+ <span className="ml-2 text-amber-600">(Cancels at period end)</span>
452
+ )}
453
+ </p>
454
+
455
+ <button
456
+ onClick={handleManageBilling}
457
+ disabled={actionLoading}
458
+ className="inline-flex items-center gap-2 px-4 py-2 bg-gray-900 text-white rounded-lg hover:bg-gray-800 disabled:opacity-50"
459
+ >
460
+ {actionLoading ? <Loader2 className="h-4 w-4 animate-spin" /> : <ExternalLink className="h-4 w-4" />}
461
+ Manage Billing
462
+ </button>
463
+ </div>
464
+ ) : (
465
+ <div className="grid md:grid-cols-2 gap-6">
466
+ {/* Pro Plan */}
467
+ <div className="bg-white rounded-xl border-2 border-indigo-500 p-6 relative">
468
+ <div className="absolute -top-3 left-4 bg-indigo-500 text-white text-xs px-2 py-1 rounded">
469
+ POPULAR
470
+ </div>
471
+ <h3 className="text-xl font-bold mb-2">Pro</h3>
472
+ <p className="text-3xl font-bold mb-4">$29<span className="text-lg text-gray-500">/mo</span></p>
473
+ <ul className="space-y-2 mb-6">
474
+ {['Unlimited projects', 'Priority support', 'API access', 'Advanced analytics'].map((feature) => (
475
+ <li key={feature} className="flex items-center gap-2 text-sm">
476
+ <Check className="h-4 w-4 text-green-500" />
477
+ {feature}
478
+ </li>
479
+ ))}
480
+ </ul>
481
+ <button
482
+ onClick={() => handleSubscribe(process.env.NEXT_PUBLIC_STRIPE_PRO_PRICE_ID || 'price_pro')}
483
+ disabled={actionLoading}
484
+ className="w-full py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-500 disabled:opacity-50"
485
+ >
486
+ {actionLoading ? 'Loading...' : 'Subscribe to Pro'}
487
+ </button>
488
+ </div>
489
+
490
+ {/* Enterprise Plan */}
491
+ <div className="bg-white rounded-xl border p-6">
492
+ <h3 className="text-xl font-bold mb-2">Enterprise</h3>
493
+ <p className="text-3xl font-bold mb-4">$99<span className="text-lg text-gray-500">/mo</span></p>
494
+ <ul className="space-y-2 mb-6">
495
+ {['Everything in Pro', 'Custom integrations', 'Dedicated account manager', 'SLA guarantee'].map((feature) => (
496
+ <li key={feature} className="flex items-center gap-2 text-sm">
497
+ <Check className="h-4 w-4 text-green-500" />
498
+ {feature}
499
+ </li>
500
+ ))}
501
+ </ul>
502
+ <button
503
+ onClick={() => handleSubscribe(process.env.NEXT_PUBLIC_STRIPE_ENTERPRISE_PRICE_ID || 'price_enterprise')}
504
+ disabled={actionLoading}
505
+ className="w-full py-2 bg-gray-900 text-white rounded-lg hover:bg-gray-800 disabled:opacity-50"
506
+ >
507
+ {actionLoading ? 'Loading...' : 'Subscribe to Enterprise'}
508
+ </button>
509
+ </div>
510
+ </div>
511
+ )}
512
+ </div>
513
+ );
514
+ }
515
+ `;
516
+
517
+ await writeFile(
518
+ path.join(projectPath, 'src/app/dashboard/billing/page.tsx'),
519
+ billingPage
520
+ );
521
+ }
@@ -1,15 +1,17 @@
1
1
  import path from 'path';
2
2
  import { writeFile, ensureDir } from '../utils/files.js';
3
3
 
4
- export async function generateSetup(projectPath) {
5
- // Create setup page directory
6
- await ensureDir(path.join(projectPath, 'src/app/setup'));
4
+ export async function generateSetup(projectPath, config) {
5
+ const isSupabaseAuth = config?.auth === 'Supabase Auth';
6
+ // Create setup page directory
7
+ await ensureDir(path.join(projectPath, 'src/app/setup'));
7
8
 
8
- // Generate setup page UI
9
- const setupPage = `'use client';
9
+ // Generate setup page UI
10
+ const setupPage = `'use client';
10
11
 
11
12
  import { useState, useEffect } from 'react';
12
- import { CheckCircle, XCircle, Loader2, AlertCircle, RefreshCw, Mail } from 'lucide-react';
13
+ import Link from 'next/link';
14
+ import { CheckCircle, XCircle, Loader2, AlertCircle, RefreshCw, Mail, ArrowLeft } from 'lucide-react';
13
15
 
14
16
  export default function SetupPage() {
15
17
  const [data, setData] = useState<any>(null);
@@ -70,6 +72,15 @@ export default function SetupPage() {
70
72
  return (
71
73
  <div className="min-h-screen bg-gray-50 p-8">
72
74
  <div className="mx-auto max-w-4xl space-y-8">
75
+ {/* Back to Home */}
76
+ <Link
77
+ href="/"
78
+ className="inline-flex items-center gap-2 text-sm font-medium text-gray-600 hover:text-gray-900 transition-colors"
79
+ >
80
+ <ArrowLeft className="h-4 w-4" />
81
+ Back to Home
82
+ </Link>
83
+
73
84
  <div className="flex items-center justify-between">
74
85
  <div>
75
86
  <h1 className="text-3xl font-bold tracking-tight">Setup & Diagnostics</h1>
@@ -240,14 +251,13 @@ function StatusIcon({ status }: { status: boolean }) {
240
251
  }
241
252
  `;
242
253
 
243
- await writeFile(path.join(projectPath, 'src/app/setup/page.tsx'), setupPage);
254
+ await writeFile(path.join(projectPath, 'src/app/setup/page.tsx'), setupPage);
244
255
 
245
- // Create API routes directory
246
- await ensureDir(path.join(projectPath, 'src/app/api/setup'));
256
+ // Create API routes directory
257
+ await ensureDir(path.join(projectPath, 'src/app/api/setup'));
247
258
 
248
- // Generate Setup API route
249
- const setupApi = `import { NextResponse } from 'next/server';
250
- import { db } from '@/lib/db';
259
+ // Generate Setup API route
260
+ const setupApi = `import { NextResponse } from 'next/server';
251
261
 
252
262
  export async function GET() {
253
263
  if (process.env.NODE_ENV !== 'development') {
@@ -259,12 +269,15 @@ export async function GET() {
259
269
  connected: false,
260
270
  userCount: 0,
261
271
  error: null as string | null,
262
- provider: 'PostgreSQL'
272
+ provider: '${isSupabaseAuth ? 'Supabase' : 'PostgreSQL'}'
263
273
  },
264
274
  auth: {
265
275
  configured: false,
266
- hasSecret: !!process.env.NEXTAUTH_SECRET,
267
- providers: [
276
+ type: '${isSupabaseAuth ? 'Supabase Auth' : 'NextAuth.js'}',
277
+ hasSecret: ${isSupabaseAuth ? '!!(process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY)' : '!!process.env.NEXTAUTH_SECRET'},
278
+ providers: ${isSupabaseAuth ? `[
279
+ { name: 'Supabase', configured: !!(process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) }
280
+ ]` : `[
268
281
  {
269
282
  name: 'Google',
270
283
  clientId: !!process.env.GOOGLE_CLIENT_ID,
@@ -277,7 +290,7 @@ export async function GET() {
277
290
  clientSecret: !!process.env.GITHUB_CLIENT_SECRET,
278
291
  configured: !!(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET)
279
292
  }
280
- ]
293
+ ]`}
281
294
  },
282
295
  stripe: {
283
296
  configured: false,
@@ -293,6 +306,10 @@ export async function GET() {
293
306
 
294
307
  // Check Database
295
308
  try {
309
+ if (!process.env.DATABASE_URL${isSupabaseAuth ? ' && !process.env.NEXT_PUBLIC_SUPABASE_URL' : ''}) {
310
+ throw new Error('DATABASE_URL is not configured in .env');
311
+ }
312
+ const { db } = await import('@/lib/db');
296
313
  const userCount = await db.user.count();
297
314
  results.database.connected = true;
298
315
  results.database.userCount = userCount;
@@ -315,12 +332,12 @@ export async function GET() {
315
332
  }
316
333
  `;
317
334
 
318
- await writeFile(path.join(projectPath, 'src/app/api/setup/route.ts'), setupApi);
335
+ await writeFile(path.join(projectPath, 'src/app/api/setup/route.ts'), setupApi);
319
336
 
320
- // Generate Test Email API route
321
- await ensureDir(path.join(projectPath, 'src/app/api/setup/test-email'));
337
+ // Generate Test Email API route
338
+ await ensureDir(path.join(projectPath, 'src/app/api/setup/test-email'));
322
339
 
323
- const testEmailApi = `import { NextResponse, NextRequest } from 'next/server';
340
+ const testEmailApi = `import { NextResponse, NextRequest } from 'next/server';
324
341
  import { Resend } from 'resend';
325
342
 
326
343
  export async function POST(req: NextRequest) {
@@ -355,5 +372,5 @@ export async function POST(req: NextRequest) {
355
372
  }
356
373
  `;
357
374
 
358
- await writeFile(path.join(projectPath, 'src/app/api/setup/test-email/route.ts'), testEmailApi);
375
+ await writeFile(path.join(projectPath, 'src/app/api/setup/test-email/route.ts'), testEmailApi);
359
376
  }