create-solostack 1.0.2 → 1.1.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 +1 -1
- package/src/generators/base.js +1 -0
- package/src/generators/setup.js +359 -0
- package/src/index.js +6 -0
package/package.json
CHANGED
package/src/generators/base.js
CHANGED
|
@@ -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);
|