create-solostack 1.0.3 → 1.2.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-solostack",
3
- "version": "1.0.3",
3
+ "version": "1.2.0",
4
4
  "description": "The complete SaaS boilerplate for indie hackers - Next.js 15 with auth, payments, database, and email",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,59 +1,50 @@
1
1
  import path from 'path';
2
- import { writeFile, renderTemplateToFile, getTemplatePath, ensureDir } from '../utils/files.js';
2
+ import { writeFile, ensureDir } from '../utils/files.js';
3
3
  import { PACKAGE_VERSIONS } from '../constants.js';
4
4
 
5
- /**
6
- * Generates the base Next.js 15 project structure
7
- * @param {string} projectPath - Path where the project should be generated
8
- * @param {string} projectName - Name of the project
9
- * @param {object} config - Project configuration
10
- */
11
- export async function generateBase(projectPath, projectName, config) {
12
- // Create directory structure
5
+ export async function generateBase(projectPath, projectName) {
6
+ // Create project directory
7
+ await ensureDir(projectPath);
8
+
9
+ // Create src/app directory
13
10
  await ensureDir(path.join(projectPath, 'src/app'));
14
- await ensureDir(path.join(projectPath, 'src/components'));
15
- await ensureDir(path.join(projectPath, 'src/lib'));
11
+
12
+ // Create public directory
16
13
  await ensureDir(path.join(projectPath, 'public'));
17
14
 
18
- //Generate package.json
15
+ // Generate package.json
19
16
  const packageJson = {
20
17
  name: projectName,
21
18
  version: '0.1.0',
22
19
  private: true,
23
- type: 'module',
20
+ type: "module",
24
21
  scripts: {
25
22
  dev: 'next dev',
26
23
  build: 'next build',
27
24
  start: 'next start',
28
25
  lint: 'next lint',
29
- 'db:push': 'prisma db push',
30
- 'db:seed': 'tsx prisma/seed.ts',
31
- 'db:studio': 'prisma studio',
26
+ "db:push": "prisma db push",
27
+ "db:seed": "tsx prisma/seed.ts",
28
+ "db:studio": "prisma studio",
29
+ postinstall: "prisma generate"
32
30
  },
33
31
  dependencies: {
34
32
  next: PACKAGE_VERSIONS.next,
35
33
  react: PACKAGE_VERSIONS.react,
36
- 'react-dom': PACKAGE_VERSIONS.reactDom,
37
- '@prisma/client': PACKAGE_VERSIONS['@prisma/client'],
34
+ 'react-dom': PACKAGE_VERSIONS['react-dom'],
38
35
  'next-auth': PACKAGE_VERSIONS['next-auth'],
39
- bcryptjs: PACKAGE_VERSIONS.bcryptjs,
36
+ '@prisma/client': PACKAGE_VERSIONS['@prisma/client'],
40
37
  stripe: PACKAGE_VERSIONS.stripe,
41
38
  resend: PACKAGE_VERSIONS.resend,
42
- '@react-email/components': PACKAGE_VERSIONS['@react-email/components'],
43
39
  zod: PACKAGE_VERSIONS.zod,
44
- 'react-hook-form': PACKAGE_VERSIONS['react-hook-form'],
45
- '@hookform/resolvers': PACKAGE_VERSIONS['@hookform/resolvers'],
46
- 'class-variance-authority': PACKAGE_VERSIONS['class-variance-authority'],
47
- clsx: PACKAGE_VERSIONS.clsx,
48
- 'tailwind-merge': PACKAGE_VERSIONS['tailwind-merge'],
49
- 'tailwindcss-animate': PACKAGE_VERSIONS['tailwindcss-animate'],
50
40
  'lucide-react': PACKAGE_VERSIONS['lucide-react'],
51
- '@radix-ui/react-dropdown-menu': PACKAGE_VERSIONS['@radix-ui/react-dropdown-menu'],
41
+ 'date-fns': PACKAGE_VERSIONS['date-fns'],
42
+ 'clsx': PACKAGE_VERSIONS.clsx,
43
+ 'tailwind-merge': PACKAGE_VERSIONS['tailwind-merge'],
52
44
  '@radix-ui/react-slot': PACKAGE_VERSIONS['@radix-ui/react-slot'],
53
- '@radix-ui/react-toast': PACKAGE_VERSIONS['@radix-ui/react-toast'],
54
- '@radix-ui/react-dialog': PACKAGE_VERSIONS['@radix-ui/react-dialog'],
55
- '@radix-ui/react-label': PACKAGE_VERSIONS['@radix-ui/react-label'],
56
- '@auth/prisma-adapter': PACKAGE_VERSIONS['@auth/prisma-adapter'],
45
+ '@radix-ui/react-label': '^2.0.2',
46
+ 'class-variance-authority': '^0.7.0',
47
+ 'tailwindcss-animate': '^1.0.7',
57
48
  },
58
49
  devDependencies: {
59
50
  typescript: PACKAGE_VERSIONS.typescript,
@@ -112,7 +103,14 @@ export async function generateBase(projectPath, projectName, config) {
112
103
 
113
104
  // Generate next.config.js
114
105
  const nextConfig = `/** @type {import('next').NextConfig} */
106
+ import path from 'path';
107
+ import { fileURLToPath } from 'url';
108
+
109
+ const __filename = fileURLToPath(import.meta.url);
110
+ const __dirname = path.dirname(__filename);
111
+
115
112
  const nextConfig = {
113
+ outputFileTracingRoot: __dirname,
116
114
  experimental: {
117
115
  serverActions: {
118
116
  bodySizeLimit: '2mb',
@@ -310,46 +308,171 @@ export default function RootLayout({
310
308
  await writeFile(path.join(projectPath, 'src/app/layout.tsx'), layoutTsx);
311
309
 
312
310
  // Generate app/page.tsx
313
- const pageTsx = `export default function Home() {
311
+ const pageTsx = `'use client';
312
+
313
+ import { useState, useEffect } from 'react';
314
+ import Link from 'next/link';
315
+ import { Rocket, Users, DollarSign, Code, Database, CreditCard, Mail, Lock, CheckCircle2, Trophy } from 'lucide-react';
316
+
317
+ export default function Home() {
318
+ const [mounted, setMounted] = useState(false);
319
+ const [mrr, setMrr] = useState(0);
320
+ const [users, setUsers] = useState(0);
321
+ const [clicks, setClicks] = useState(0);
322
+ const [shipped, setShipped] = useState(0);
323
+
324
+ useEffect(() => {
325
+ setMounted(true);
326
+ }, []);
327
+
328
+ const ship = () => {
329
+ setClicks(prev => prev + 1);
330
+ setShipped(prev => prev + 1);
331
+ setUsers(prev => prev + Math.floor(Math.random() * 3) + 1);
332
+
333
+ // Revenue logic
334
+ if (Math.random() > 0.7) {
335
+ setMrr(prev => prev + Math.floor(Math.random() * 20) + 10);
336
+ }
337
+ };
338
+
339
+ if (!mounted) return (
340
+ <div className="min-h-screen bg-black text-white flex items-center justify-center">
341
+ <div className="animate-pulse">Loading SaaS...</div>
342
+ </div>
343
+ );
344
+
314
345
  return (
315
- <main className="flex min-h-screen flex-col items-center justify-center p-24">
316
- <div className="z-10 max-w-5xl w-full items-center justify-center font-mono text-sm">
317
- <h1 className="text-4xl font-bold text-center mb-4">
318
- Welcome to ${projectName}
319
- </h1>
320
- <p className="text-center text-muted-foreground">
321
- Built with SoloStack - Your Next.js SaaS boilerplate
322
- </p>
323
- <div className="mt-8 grid grid-cols-1 md:grid-cols-2 gap-4">
324
- <div className="p-6 border rounded-lg">
325
- <h2 className="text-xl font-semibold mb-2">✅ Authentication</h2>
326
- <p className="text-sm text-muted-foreground">
327
- NextAuth.js configured with email and OAuth providers
328
- </p>
346
+ <main className="min-h-screen bg-black text-white font-sans selection:bg-indigo-500/30 overflow-hidden">
347
+ {/* Background Gradient */}
348
+ <div className="fixed inset-0 z-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-indigo-900/20 via-black to-black pointer-events-none" />
349
+
350
+ <div className="relative z-10 max-w-6xl mx-auto px-4 py-12">
351
+ {/* Header */}
352
+ <header className="flex items-center justify-between mb-16">
353
+ <div className="flex items-center gap-2">
354
+ <div className="h-8 w-8 bg-indigo-600 rounded-lg flex items-center justify-center">
355
+ <Code className="h-5 w-5 text-white" />
356
+ </div>
357
+ <span className="font-bold text-xl tracking-tight">${projectName}</span>
329
358
  </div>
330
- <div className="p-6 border rounded-lg">
331
- <h2 className="text-xl font-semibold mb-2">✅ Database</h2>
332
- <p className="text-sm text-muted-foreground">
333
- Prisma + PostgreSQL ready to use
334
- </p>
335
- </div>
336
- <div className="p-6 border rounded-lg">
337
- <h2 className="text-xl font-semibold mb-2">✅ Payments</h2>
338
- <p className="text-sm text-muted-foreground">
339
- Stripe integration for subscriptions
359
+
360
+ {process.env.NODE_ENV === 'development' && (
361
+ <Link
362
+ href="/setup"
363
+ className="text-sm text-zinc-400 hover:text-white transition-colors flex items-center gap-2 border border-zinc-800 rounded-full px-4 py-1.5 hover:border-zinc-600"
364
+ >
365
+ <CheckCircle2 className="h-4 w-4" />
366
+ Check Diagnostics
367
+ </Link>
368
+ )}
369
+ </header>
370
+
371
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center mb-24">
372
+ {/* Game Section */}
373
+ <div className="space-y-8">
374
+ <div className="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-indigo-500/10 text-indigo-400 text-sm border border-indigo-500/20">
375
+ <Rocket className="h-4 w-4" />
376
+ <span>Interactive SaaS Simulator</span>
377
+ </div>
378
+
379
+ <h1 className="text-5xl md:text-6xl font-bold tracking-tight leading-none bg-clip-text text-transparent bg-gradient-to-r from-white to-zinc-500">
380
+ Build your SaaS <br />
381
+ <span className="text-indigo-500">in minutes.</span>
382
+ </h1>
383
+
384
+ <p className="text-xl text-zinc-400 max-w-lg leading-relaxed">
385
+ The ultimate Next.js 15 boilerplate for indie hackers. Authentication, Database, Payments, and Email - pre-configured and ready to ship.
340
386
  </p>
387
+
388
+ {/* Game UI */}
389
+ <div className="bg-zinc-900/50 backdrop-blur-sm border border-zinc-800 rounded-2xl p-6 shadow-2xl relative overflow-hidden group/game">
390
+ <div className="absolute inset-0 bg-indigo-500/5 group-hover/game:bg-indigo-500/10 transition-colors pointer-events-none" />
391
+
392
+ <div className="grid grid-cols-3 gap-4 mb-6 relative z-10">
393
+ <div className="bg-black/50 p-4 rounded-xl border border-zinc-800">
394
+ <div className="text-zinc-500 text-xs uppercase font-semibold mb-1">MRR</div>
395
+ <div className="text-2xl font-mono text-green-400 flex items-center">
396
+ <DollarSign className="h-5 w-5 mr-1" />
397
+ {mrr}
398
+ </div>
399
+ </div>
400
+ <div className="bg-black/50 p-4 rounded-xl border border-zinc-800">
401
+ <div className="text-zinc-500 text-xs uppercase font-semibold mb-1">Users</div>
402
+ <div className="text-2xl font-mono text-blue-400 flex items-center">
403
+ <Users className="h-5 w-5 mr-1" />
404
+ {users}
405
+ </div>
406
+ </div>
407
+ <div className="bg-black/50 p-4 rounded-xl border border-zinc-800">
408
+ <div className="text-zinc-500 text-xs uppercase font-semibold mb-1">Shipped</div>
409
+ <div className="text-2xl font-mono text-purple-400 flex items-center">
410
+ <Trophy className="h-5 w-5 mr-1" />
411
+ {shipped}
412
+ </div>
413
+ </div>
414
+ </div>
415
+
416
+ <button
417
+ onClick={ship}
418
+ className="w-full bg-indigo-600 hover:bg-indigo-500 text-white font-bold py-4 rounded-xl transition-all active:scale-95 shadow-[0_0_20px_rgba(79,70,229,0.3)] hover:shadow-[0_0_30px_rgba(79,70,229,0.5)] flex items-center justify-center gap-3 group relative z-10"
419
+ >
420
+ <Rocket className="h-6 w-6 group-hover:rotate-12 transition-transform" />
421
+ SHIP FEATURE
422
+ </button>
423
+
424
+ <p className="text-center text-zinc-500 text-xs mt-4 relative z-10">
425
+ Click to simulate your indie hacker journey! 🚀
426
+ </p>
427
+ </div>
341
428
  </div>
342
- <div className="p-6 border rounded-lg">
343
- <h2 className="text-xl font-semibold mb-2">✅ Emails</h2>
344
- <p className="text-sm text-muted-foreground">
345
- Resend configured for transactional emails
346
- </p>
429
+
430
+ {/* Cards Section */}
431
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
432
+ <div className="p-6 rounded-2xl bg-zinc-900/30 border border-zinc-800 hover:border-zinc-600 transition-colors group">
433
+ <div className="h-10 w-10 bg-zinc-800 rounded-lg flex items-center justify-center mb-4 group-hover:bg-zinc-700 transition-colors">
434
+ <Lock className="h-5 w-5 text-indigo-400" />
435
+ </div>
436
+ <h3 className="text-lg font-semibold mb-2 text-white">Authentication</h3>
437
+ <p className="text-zinc-400 text-sm">
438
+ NextAuth.js v5 pre-configured with Google & GitHub OAuth.
439
+ </p>
440
+ </div>
441
+
442
+ <div className="p-6 rounded-2xl bg-zinc-900/30 border border-zinc-800 hover:border-zinc-600 transition-colors group">
443
+ <div className="h-10 w-10 bg-zinc-800 rounded-lg flex items-center justify-center mb-4 group-hover:bg-zinc-700 transition-colors">
444
+ <Database className="h-5 w-5 text-emerald-400" />
445
+ </div>
446
+ <h3 className="text-lg font-semibold mb-2 text-white">Database</h3>
447
+ <p className="text-zinc-400 text-sm">
448
+ Prisma ORM with PostgreSQL. Complete schema for Users and Subs.
449
+ </p>
450
+ </div>
451
+
452
+ <div className="p-6 rounded-2xl bg-zinc-900/30 border border-zinc-800 hover:border-zinc-600 transition-colors group">
453
+ <div className="h-10 w-10 bg-zinc-800 rounded-lg flex items-center justify-center mb-4 group-hover:bg-zinc-700 transition-colors">
454
+ <CreditCard className="h-5 w-5 text-pink-400" />
455
+ </div>
456
+ <h3 className="text-lg font-semibold mb-2 text-white">Payments</h3>
457
+ <p className="text-zinc-400 text-sm">
458
+ Stripe integration with Webhook idempotency and Customer Portal.
459
+ </p>
460
+ </div>
461
+
462
+ <div className="p-6 rounded-2xl bg-zinc-900/30 border border-zinc-800 hover:border-zinc-600 transition-colors group">
463
+ <div className="h-10 w-10 bg-zinc-800 rounded-lg flex items-center justify-center mb-4 group-hover:bg-zinc-700 transition-colors">
464
+ <Mail className="h-5 w-5 text-blue-400" />
465
+ </div>
466
+ <h3 className="text-lg font-semibold mb-2 text-white">Emails</h3>
467
+ <p className="text-zinc-400 text-sm">
468
+ Resend + React Email based transactional emails.
469
+ </p>
470
+ </div>
347
471
  </div>
348
472
  </div>
349
- <div className="mt-8 text-center">
350
- <p className="text-sm text-muted-foreground">
351
- Get started by editing <code className="font-mono bg-muted px-2 py-1 rounded">src/app/page.tsx</code>
352
- </p>
473
+
474
+ <div className="text-center text-zinc-500 text-sm">
475
+ Get started by editing <code className="bg-zinc-900 border border-zinc-700 px-2 py-1 rounded text-zinc-300 font-mono">src/app/page.tsx</code>
353
476
  </div>
354
477
  </div>
355
478
  </main>
@@ -0,0 +1,359 @@
1
+ import path from 'path';
2
+ import { writeFile, ensureDir } from '../utils/files.js';
3
+
4
+ export async function generateSetup(projectPath) {
5
+ // Create setup page directory
6
+ await ensureDir(path.join(projectPath, 'src/app/setup'));
7
+
8
+ // Generate setup page UI
9
+ const setupPage = `'use client';
10
+
11
+ import { useState, useEffect } from 'react';
12
+ import { CheckCircle, XCircle, Loader2, AlertCircle, RefreshCw, Mail } from 'lucide-react';
13
+
14
+ export default function SetupPage() {
15
+ const [data, setData] = useState<any>(null);
16
+ const [loading, setLoading] = useState(true);
17
+ const [emailSending, setEmailSending] = useState(false);
18
+ const [emailStatus, setEmailStatus] = useState<{ success: boolean; message: string } | null>(null);
19
+
20
+ const checkStatus = async () => {
21
+ setLoading(true);
22
+ try {
23
+ const res = await fetch('/api/setup');
24
+ const json = await res.json();
25
+ setData(json);
26
+ } catch (error) {
27
+ console.error('Failed to fetch setup status:', error);
28
+ } finally {
29
+ setLoading(false);
30
+ }
31
+ };
32
+
33
+ useEffect(() => {
34
+ checkStatus();
35
+ }, []);
36
+
37
+ const sendTestEmail = async (e: React.FormEvent) => {
38
+ e.preventDefault();
39
+ setEmailSending(true);
40
+ setEmailStatus(null);
41
+ const formData = new FormData(e.target as HTMLFormElement);
42
+ const email = formData.get('email');
43
+
44
+ try {
45
+ const res = await fetch('/api/setup/test-email', {
46
+ method: 'POST',
47
+ headers: { 'Content-Type': 'application/json' },
48
+ body: JSON.stringify({ email }),
49
+ });
50
+ const json = await res.json();
51
+ setEmailStatus({
52
+ success: res.ok,
53
+ message: json.message || (res.ok ? 'Email sent successfully!' : 'Failed to send email'),
54
+ });
55
+ } catch (error) {
56
+ setEmailStatus({ success: false, message: 'Failed to send email' });
57
+ } finally {
58
+ setEmailSending(false);
59
+ }
60
+ };
61
+
62
+ if (process.env.NODE_ENV !== 'development') {
63
+ return (
64
+ <div className="flex min-h-screen items-center justify-center">
65
+ <p className="text-muted-foreground">This page is only available in development mode.</p>
66
+ </div>
67
+ );
68
+ }
69
+
70
+ return (
71
+ <div className="min-h-screen bg-gray-50 p-8">
72
+ <div className="mx-auto max-w-4xl space-y-8">
73
+ <div className="flex items-center justify-between">
74
+ <div>
75
+ <h1 className="text-3xl font-bold tracking-tight">Setup & Diagnostics</h1>
76
+ <p className="text-gray-500">Check your boilerplate configuration status.</p>
77
+ </div>
78
+ <button
79
+ onClick={checkStatus}
80
+ disabled={loading}
81
+ className="flex items-center gap-2 rounded-md bg-white px-4 py-2 text-sm font-medium shadow-sm ring-1 ring-gray-200 hover:bg-gray-50 disabled:opacity-50"
82
+ >
83
+ <RefreshCw className={\`h-4 w-4 \${loading ? 'animate-spin' : ''}\`} />
84
+ Refresh
85
+ </button>
86
+ </div>
87
+
88
+ {loading ? (
89
+ <div className="flex h-64 items-center justify-center rounded-lg border bg-white shadow-sm">
90
+ <Loader2 className="h-8 w-8 animate-spin text-indigo-600" />
91
+ </div>
92
+ ) : (
93
+ <div className="grid gap-6 md:grid-cols-2">
94
+ {/* Database Card */}
95
+ <div className="rounded-lg border bg-white p-6 shadow-sm">
96
+ <div className="flex items-center justify-between mb-4">
97
+ <h2 className="text-lg font-semibold">Database</h2>
98
+ <StatusBadge status={data?.database?.connected} />
99
+ </div>
100
+ <div className="space-y-3">
101
+ <div className="flex justify-between text-sm">
102
+ <span className="text-gray-500">Connection</span>
103
+ <span className="font-mono">{data?.database?.provider}</span>
104
+ </div>
105
+ {data?.database?.error && (
106
+ <div className="rounded-md bg-red-50 p-3">
107
+ <p className="text-sm text-red-800">{data.database.error}</p>
108
+ </div>
109
+ )}
110
+ {data?.database?.connected && (
111
+ <div className="rounded-md bg-green-50 p-3">
112
+ <p className="text-sm text-green-800">
113
+ Successfully connected. Found {data.database.userCount} users.
114
+ </p>
115
+ </div>
116
+ )}
117
+ </div>
118
+ </div>
119
+
120
+ {/* Authentication Card */}
121
+ <div className="rounded-lg border bg-white p-6 shadow-sm">
122
+ <div className="flex items-center justify-between mb-4">
123
+ <h2 className="text-lg font-semibold">Authentication</h2>
124
+ <StatusBadge status={data?.auth?.configured} />
125
+ </div>
126
+ <div className="space-y-4">
127
+ <div className="flex justify-between text-sm border-b pb-2">
128
+ <span className="text-gray-500">NextAuth Secret</span>
129
+ <StatusIcon status={data?.auth?.hasSecret} />
130
+ </div>
131
+
132
+ <div>
133
+ <h3 className="text-sm font-medium text-gray-900 mb-2">Providers</h3>
134
+ <div className="space-y-2">
135
+ {data?.auth?.providers?.map((p: any) => (
136
+ <div key={p.name} className="flex items-center justify-between text-sm">
137
+ <span className="text-gray-600">{p.name}</span>
138
+ <div className="flex items-center gap-2">
139
+ <span className="text-xs text-gray-400">
140
+ {p.clientId ? 'ID Set' : 'Missing ID'}
141
+ </span>
142
+ <StatusIcon status={p.configured} />
143
+ </div>
144
+ </div>
145
+ ))}
146
+ </div>
147
+ </div>
148
+ </div>
149
+ </div>
150
+
151
+ {/* Payments Card */}
152
+ <div className="rounded-lg border bg-white p-6 shadow-sm">
153
+ <div className="flex items-center justify-between mb-4">
154
+ <h2 className="text-lg font-semibold">Payments (Stripe)</h2>
155
+ <StatusBadge status={data?.stripe?.configured} />
156
+ </div>
157
+ <div className="space-y-3">
158
+ <div className="flex justify-between text-sm">
159
+ <span className="text-gray-500">Secret Key</span>
160
+ <StatusIcon status={data?.stripe?.hasSecretKey} />
161
+ </div>
162
+ <div className="flex justify-between text-sm">
163
+ <span className="text-gray-500">Publishable Key</span>
164
+ <StatusIcon status={data?.stripe?.hasPublishableKey} />
165
+ </div>
166
+ <div className="flex justify-between text-sm">
167
+ <span className="text-gray-500">Webhook Secret</span>
168
+ <StatusIcon status={data?.stripe?.hasWebhookSecret} />
169
+ </div>
170
+ </div>
171
+ </div>
172
+
173
+ {/* Email Card */}
174
+ <div className="rounded-lg border bg-white p-6 shadow-sm">
175
+ <div className="flex items-center justify-between mb-4">
176
+ <h2 className="text-lg font-semibold">Email (Resend)</h2>
177
+ <StatusBadge status={data?.email?.configured} />
178
+ </div>
179
+ <div className="space-y-4">
180
+ <div className="flex justify-between text-sm">
181
+ <span className="text-gray-500">API Key</span>
182
+ <StatusIcon status={data?.email?.hasApiKey} />
183
+ </div>
184
+
185
+ {data?.email?.configured && (
186
+ <form onSubmit={sendTestEmail} className="mt-4 pt-4 border-t">
187
+ <label className="block text-sm font-medium text-gray-700 mb-2">
188
+ Send Test Email
189
+ </label>
190
+ <div className="flex gap-2">
191
+ <input
192
+ required
193
+ type="email"
194
+ name="email"
195
+ placeholder="you@example.com"
196
+ className="flex-1 rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
197
+ />
198
+ <button
199
+ type="submit"
200
+ disabled={emailSending}
201
+ className="rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white hover:bg-indigo-500 disabled:opacity-50"
202
+ >
203
+ {emailSending ? <Loader2 className="h-4 w-4 animate-spin" /> : <Mail className="h-4 w-4" />}
204
+ </button>
205
+ </div>
206
+ {emailStatus && (
207
+ <p className={\`mt-2 text-xs \${emailStatus.success ? 'text-green-600' : 'text-red-600'}\`}>
208
+ {emailStatus.message}
209
+ </p>
210
+ )}
211
+ </form>
212
+ )}
213
+ </div>
214
+ </div>
215
+ </div>
216
+ )}
217
+ </div>
218
+ </div>
219
+ );
220
+ }
221
+
222
+ function StatusBadge({ status }: { status: boolean }) {
223
+ return (
224
+ <span
225
+ className={\`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium \${
226
+ status ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
227
+ }\`}
228
+ >
229
+ {status ? 'Configured' : 'Incomplete'}
230
+ </span>
231
+ );
232
+ }
233
+
234
+ function StatusIcon({ status }: { status: boolean }) {
235
+ return status ? (
236
+ <CheckCircle className="h-4 w-4 text-green-500" />
237
+ ) : (
238
+ <XCircle className="h-4 w-4 text-red-500" />
239
+ );
240
+ }
241
+ `;
242
+
243
+ await writeFile(path.join(projectPath, 'src/app/setup/page.tsx'), setupPage);
244
+
245
+ // Create API routes directory
246
+ await ensureDir(path.join(projectPath, 'src/app/api/setup'));
247
+
248
+ // Generate Setup API route
249
+ const setupApi = `import { NextResponse } from 'next/server';
250
+ import { db } from '@/lib/db';
251
+
252
+ export async function GET() {
253
+ if (process.env.NODE_ENV !== 'development') {
254
+ return NextResponse.json({ error: 'Not available in production' }, { status: 403 });
255
+ }
256
+
257
+ const results = {
258
+ database: {
259
+ connected: false,
260
+ userCount: 0,
261
+ error: null as string | null,
262
+ provider: 'PostgreSQL'
263
+ },
264
+ auth: {
265
+ configured: false,
266
+ hasSecret: !!process.env.NEXTAUTH_SECRET,
267
+ providers: [
268
+ {
269
+ name: 'Google',
270
+ clientId: !!process.env.GOOGLE_CLIENT_ID,
271
+ clientSecret: !!process.env.GOOGLE_CLIENT_SECRET,
272
+ configured: !!(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET)
273
+ },
274
+ {
275
+ name: 'GitHub',
276
+ clientId: !!process.env.GITHUB_CLIENT_ID,
277
+ clientSecret: !!process.env.GITHUB_CLIENT_SECRET,
278
+ configured: !!(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET)
279
+ }
280
+ ]
281
+ },
282
+ stripe: {
283
+ configured: false,
284
+ hasSecretKey: !!process.env.STRIPE_SECRET_KEY,
285
+ hasPublishableKey: !!process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
286
+ hasWebhookSecret: !!process.env.STRIPE_WEBHOOK_SECRET,
287
+ },
288
+ email: {
289
+ configured: false,
290
+ hasApiKey: !!process.env.RESEND_API_KEY,
291
+ }
292
+ };
293
+
294
+ // Check Database
295
+ try {
296
+ const userCount = await db.user.count();
297
+ results.database.connected = true;
298
+ results.database.userCount = userCount;
299
+ } catch (error: any) {
300
+ results.database.error = error.message;
301
+ }
302
+
303
+ // Check Auth
304
+ results.auth.configured = results.auth.hasSecret;
305
+
306
+ // Check Stripe
307
+ results.stripe.configured =
308
+ results.stripe.hasSecretKey &&
309
+ results.stripe.hasPublishableKey;
310
+
311
+ // Check Email
312
+ results.email.configured = results.email.hasApiKey;
313
+
314
+ return NextResponse.json(results);
315
+ }
316
+ `;
317
+
318
+ await writeFile(path.join(projectPath, 'src/app/api/setup/route.ts'), setupApi);
319
+
320
+ // Generate Test Email API route
321
+ await ensureDir(path.join(projectPath, 'src/app/api/setup/test-email'));
322
+
323
+ const testEmailApi = `import { NextResponse, NextRequest } from 'next/server';
324
+ import { Resend } from 'resend';
325
+
326
+ export async function POST(req: NextRequest) {
327
+ if (process.env.NODE_ENV !== 'development') {
328
+ return NextResponse.json({ error: 'Not available in production' }, { status: 403 });
329
+ }
330
+
331
+ try {
332
+ const { email } = await req.json();
333
+
334
+ if (!process.env.RESEND_API_KEY) {
335
+ throw new Error('RESEND_API_KEY is not configured');
336
+ }
337
+
338
+ const resend = new Resend(process.env.RESEND_API_KEY);
339
+
340
+ const { data, error } = await resend.emails.send({
341
+ from: process.env.FROM_EMAIL || 'onboarding@resend.dev',
342
+ to: email,
343
+ subject: 'Test Email from Solo Stack',
344
+ html: '<p>Congrats! Your email configuration is working correctly. 🎉</p>'
345
+ });
346
+
347
+ if (error) {
348
+ return NextResponse.json({ message: error.message }, { status: 400 });
349
+ }
350
+
351
+ return NextResponse.json({ message: 'Email sent successfully' });
352
+ } catch (error: any) {
353
+ return NextResponse.json({ message: error.message }, { status: 500 });
354
+ }
355
+ }
356
+ `;
357
+
358
+ await writeFile(path.join(projectPath, 'src/app/api/setup/test-email/route.ts'), testEmailApi);
359
+ }
package/src/index.js CHANGED
@@ -13,6 +13,7 @@ import { generateDatabase } from './generators/database.js';
13
13
  import { generateAuth } from './generators/auth.js';
14
14
  import { generatePayments } from './generators/payments.js';
15
15
  import { generateEmails } from './generators/emails.js';
16
+ import { generateSetup } from './generators/setup.js';
16
17
  import { generateUI } from './generators/ui.js';
17
18
  import {
18
19
  AUTH_PROVIDERS,
@@ -150,6 +151,11 @@ export async function main() {
150
151
  spinner.succeed('Added UI components (shadcn/ui)');
151
152
  }
152
153
 
154
+ // Generate setup & diagnostics
155
+ spinner = ora('Adding diagnostics page').start();
156
+ await generateSetup(projectPath);
157
+ spinner.succeed('Added diagnostics page (/setup)');
158
+
153
159
  // Install dependencies
154
160
  spinner = ora('Installing dependencies (this may take a minute...)').start();
155
161
  await installPackages(projectPath);