create-solostack 1.0.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/LICENSE +21 -0
- package/README.md +262 -0
- package/bin/cli.js +13 -0
- package/package.json +45 -0
- package/src/constants.js +94 -0
- package/src/generators/auth.js +595 -0
- package/src/generators/base.js +592 -0
- package/src/generators/database.js +365 -0
- package/src/generators/emails.js +404 -0
- package/src/generators/payments.js +541 -0
- package/src/generators/ui.js +368 -0
- package/src/index.js +374 -0
- package/src/utils/files.js +81 -0
- package/src/utils/git.js +69 -0
- package/src/utils/logger.js +62 -0
- package/src/utils/packages.js +75 -0
- package/src/utils/validate.js +17 -0
package/src/index.js
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { program } from 'commander';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { validateProjectName } from './utils/validate.js';
|
|
7
|
+
import { printSuccess, printError } from './utils/logger.js';
|
|
8
|
+
import { ensureDir } from './utils/files.js';
|
|
9
|
+
import { installPackages } from './utils/packages.js';
|
|
10
|
+
import { initGit } from './utils/git.js';
|
|
11
|
+
import { generateBase } from './generators/base.js';
|
|
12
|
+
import { generateDatabase } from './generators/database.js';
|
|
13
|
+
import { generateAuth } from './generators/auth.js';
|
|
14
|
+
import { generatePayments } from './generators/payments.js';
|
|
15
|
+
import { generateEmails } from './generators/emails.js';
|
|
16
|
+
import { generateUI } from './generators/ui.js';
|
|
17
|
+
import {
|
|
18
|
+
AUTH_PROVIDERS,
|
|
19
|
+
DATABASES,
|
|
20
|
+
PAYMENT_PROVIDERS,
|
|
21
|
+
EMAIL_PROVIDERS,
|
|
22
|
+
} from './constants.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Main CLI function - orchestrates the entire project generation
|
|
26
|
+
*/
|
|
27
|
+
export async function main() {
|
|
28
|
+
// Display banner
|
|
29
|
+
console.log(chalk.cyan(`
|
|
30
|
+
____ _ ____ _ _
|
|
31
|
+
/ ___| ___ | | ___ / ___|| |_ __ _ ___| | __
|
|
32
|
+
\\___ \\ / _ \\| |/ _ \\\\___ \\| __/ _\` |/ __| |/ /
|
|
33
|
+
___) | (_) | | (_) |___) | || (_| | (__| <
|
|
34
|
+
|____/ \\___/|_|\\___/|____/ \\__\\__,_|\\___|_|\\_\\
|
|
35
|
+
|
|
36
|
+
The complete SaaS boilerplate for indie hackers
|
|
37
|
+
`));
|
|
38
|
+
|
|
39
|
+
// Parse command line arguments
|
|
40
|
+
program
|
|
41
|
+
.name('create-solostack')
|
|
42
|
+
.description('Generate a production-ready Next.js SaaS boilerplate')
|
|
43
|
+
.argument('[project-name]', 'Name of your project')
|
|
44
|
+
.parse();
|
|
45
|
+
|
|
46
|
+
let projectName = program.args[0];
|
|
47
|
+
|
|
48
|
+
// If no project name provided, ask for it
|
|
49
|
+
if (!projectName) {
|
|
50
|
+
const answers = await inquirer.prompt([
|
|
51
|
+
{
|
|
52
|
+
type: 'input',
|
|
53
|
+
name: 'projectName',
|
|
54
|
+
message: 'Project name:',
|
|
55
|
+
default: 'my-saas-app',
|
|
56
|
+
validate: validateProjectName,
|
|
57
|
+
},
|
|
58
|
+
]);
|
|
59
|
+
projectName = answers.projectName;
|
|
60
|
+
} else {
|
|
61
|
+
const validation = validateProjectName(projectName);
|
|
62
|
+
if (validation !== true) {
|
|
63
|
+
printError(validation);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Ask configuration questions
|
|
69
|
+
const config = await inquirer.prompt([
|
|
70
|
+
{
|
|
71
|
+
type: 'list',
|
|
72
|
+
name: 'auth',
|
|
73
|
+
message: 'Choose authentication:',
|
|
74
|
+
choices: AUTH_PROVIDERS,
|
|
75
|
+
default: AUTH_PROVIDERS[0],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
type: 'list',
|
|
79
|
+
name: 'database',
|
|
80
|
+
message: 'Choose database:',
|
|
81
|
+
choices: DATABASES,
|
|
82
|
+
default: DATABASES[0],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
type: 'list',
|
|
86
|
+
name: 'payments',
|
|
87
|
+
message: 'Choose payment provider:',
|
|
88
|
+
choices: PAYMENT_PROVIDERS,
|
|
89
|
+
default: PAYMENT_PROVIDERS[0],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
type: 'list',
|
|
93
|
+
name: 'email',
|
|
94
|
+
message: 'Choose email provider:',
|
|
95
|
+
choices: EMAIL_PROVIDERS,
|
|
96
|
+
default: EMAIL_PROVIDERS[0],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
type: 'confirm',
|
|
100
|
+
name: 'includeUI',
|
|
101
|
+
message: 'Include shadcn/ui components?',
|
|
102
|
+
default: true,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
type: 'confirm',
|
|
106
|
+
name: 'initGit',
|
|
107
|
+
message: 'Initialize git repository?',
|
|
108
|
+
default: true,
|
|
109
|
+
},
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
const projectPath = path.join(process.cwd(), projectName);
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
// Start generation
|
|
116
|
+
console.log(chalk.cyan('\\nāļø Creating your SaaS boilerplate...\\n'));
|
|
117
|
+
|
|
118
|
+
// Ensure project directory exists
|
|
119
|
+
await ensureDir(projectPath);
|
|
120
|
+
|
|
121
|
+
// Generate base Next.js project
|
|
122
|
+
let spinner = ora('Generating Next.js project').start();
|
|
123
|
+
await generateBase(projectPath, projectName, config);
|
|
124
|
+
spinner.succeed('Generated Next.js project');
|
|
125
|
+
|
|
126
|
+
// Generate database integration
|
|
127
|
+
spinner = ora('Configuring database').start();
|
|
128
|
+
await generateDatabase(projectPath, config.database);
|
|
129
|
+
spinner.succeed('Configured database (Prisma + PostgreSQL)');
|
|
130
|
+
|
|
131
|
+
// Generate authentication
|
|
132
|
+
spinner = ora('Configuring authentication').start();
|
|
133
|
+
await generateAuth(projectPath, config.auth);
|
|
134
|
+
spinner.succeed('Configured authentication (NextAuth.js)');
|
|
135
|
+
|
|
136
|
+
// Generate payments
|
|
137
|
+
spinner = ora('Configuring payments').start();
|
|
138
|
+
await generatePayments(projectPath, config.payments);
|
|
139
|
+
spinner.succeed('Configured payments (Stripe)');
|
|
140
|
+
|
|
141
|
+
// Generate emails
|
|
142
|
+
spinner = ora('Configuring emails').start();
|
|
143
|
+
await generateEmails(projectPath, config.email);
|
|
144
|
+
spinner.succeed('Configured emails (Resend)');
|
|
145
|
+
|
|
146
|
+
// Generate UI components
|
|
147
|
+
if (config.includeUI) {
|
|
148
|
+
spinner = ora('Adding UI components').start();
|
|
149
|
+
await generateUI(projectPath);
|
|
150
|
+
spinner.succeed('Added UI components (shadcn/ui)');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Install dependencies
|
|
154
|
+
spinner = ora('Installing dependencies (this may take a minute...)').start();
|
|
155
|
+
await installPackages(projectPath);
|
|
156
|
+
spinner.succeed('Installed dependencies');
|
|
157
|
+
|
|
158
|
+
// Initialize git
|
|
159
|
+
if (config.initGit) {
|
|
160
|
+
spinner = ora('Initializing git repository').start();
|
|
161
|
+
await initGit(projectPath);
|
|
162
|
+
spinner.succeed('Initialized git repository');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Success message
|
|
166
|
+
printSuccess(projectName, projectPath);
|
|
167
|
+
|
|
168
|
+
// Optional setup wizard
|
|
169
|
+
console.log(); // Empty line for spacing
|
|
170
|
+
const { setupNow } = await inquirer.prompt([
|
|
171
|
+
{
|
|
172
|
+
type: 'confirm',
|
|
173
|
+
name: 'setupNow',
|
|
174
|
+
message: 'Would you like to set up your API keys now?',
|
|
175
|
+
default: true,
|
|
176
|
+
},
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
if (setupNow) {
|
|
180
|
+
await runSetupWizard(projectPath);
|
|
181
|
+
} else {
|
|
182
|
+
console.log(chalk.yellow('\nā¹ļø You can set up your API keys later by:'));
|
|
183
|
+
console.log(chalk.white(' 1. Copy .env.example to .env'));
|
|
184
|
+
console.log(chalk.white(' 2. Add your API keys to .env'));
|
|
185
|
+
console.log(chalk.white(' 3. Run: npm run dev\n'));
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
printError(error.message);
|
|
189
|
+
console.error(error);
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Interactive setup wizard for API keys
|
|
196
|
+
*/
|
|
197
|
+
async function runSetupWizard(projectPath) {
|
|
198
|
+
console.log(chalk.cyan('\nš API Key Setup'));
|
|
199
|
+
console.log(chalk.gray("Let's configure your environment variables.\n"));
|
|
200
|
+
|
|
201
|
+
const crypto = await import('crypto');
|
|
202
|
+
|
|
203
|
+
// Database setup
|
|
204
|
+
console.log(chalk.cyan('š Database'));
|
|
205
|
+
const { databaseUrl } = await inquirer.prompt([
|
|
206
|
+
{
|
|
207
|
+
type: 'input',
|
|
208
|
+
name: 'databaseUrl',
|
|
209
|
+
message: 'PostgreSQL connection string:',
|
|
210
|
+
default: 'postgresql://user:password@localhost:5432/dbname',
|
|
211
|
+
validate: (input) => {
|
|
212
|
+
if (!input) return 'Database URL is required';
|
|
213
|
+
if (!input.startsWith('postgres')) return 'Must be a PostgreSQL connection string';
|
|
214
|
+
return true;
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
]);
|
|
218
|
+
console.log(chalk.gray('š” Get from: Neon (neon.tech) or Supabase (supabase.com)\n'));
|
|
219
|
+
|
|
220
|
+
// NextAuth setup
|
|
221
|
+
console.log(chalk.cyan('š NextAuth'));
|
|
222
|
+
const { nextauthSecret, nextauthUrl } = await inquirer.prompt([
|
|
223
|
+
{
|
|
224
|
+
type: 'input',
|
|
225
|
+
name: 'nextauthSecret',
|
|
226
|
+
message: 'NEXTAUTH_SECRET (press Enter to auto-generate):',
|
|
227
|
+
default: () => crypto.randomBytes(32).toString('base64'),
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
type: 'input',
|
|
231
|
+
name: 'nextauthUrl',
|
|
232
|
+
message: 'NEXTAUTH_URL:',
|
|
233
|
+
default: 'http://localhost:3000',
|
|
234
|
+
},
|
|
235
|
+
]);
|
|
236
|
+
console.log(chalk.gray('š” Generated secure secret automatically\n'));
|
|
237
|
+
|
|
238
|
+
// Stripe setup
|
|
239
|
+
console.log(chalk.cyan('š Stripe'));
|
|
240
|
+
const { stripeSecretKey, stripePublishableKey, stripeProPrice, stripeEnterprisePrice } = await inquirer.prompt([
|
|
241
|
+
{
|
|
242
|
+
type: 'input',
|
|
243
|
+
name: 'stripeSecretKey',
|
|
244
|
+
message: 'Stripe Secret Key (sk_test_...):',
|
|
245
|
+
validate: (input) => {
|
|
246
|
+
if (!input) return 'Stripe Secret Key is required';
|
|
247
|
+
if (!input.startsWith('sk_')) return 'Must start with sk_test_ or sk_live_';
|
|
248
|
+
return true;
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
type: 'input',
|
|
253
|
+
name: 'stripePublishableKey',
|
|
254
|
+
message: 'Stripe Publishable Key (pk_test_...):',
|
|
255
|
+
validate: (input) => {
|
|
256
|
+
if (!input) return 'Stripe Publishable Key is required';
|
|
257
|
+
if (!input.startsWith('pk_')) return 'Must start with pk_test_ or pk_live_';
|
|
258
|
+
return true;
|
|
259
|
+
},
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
type: 'input',
|
|
263
|
+
name: 'stripeProPrice',
|
|
264
|
+
message: 'Pro Plan Price ID (price_...):',
|
|
265
|
+
default: 'price_pro',
|
|
266
|
+
validate: (input) => input ? true : 'Price ID is required',
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
type: 'input',
|
|
270
|
+
name: 'stripeEnterprisePrice',
|
|
271
|
+
message: 'Enterprise Plan Price ID (price_...):',
|
|
272
|
+
default: 'price_enterprise',
|
|
273
|
+
validate: (input) => input ? true : 'Price ID is required',
|
|
274
|
+
},
|
|
275
|
+
]);
|
|
276
|
+
console.log(chalk.gray('š” Get from: dashboard.stripe.com/test/products\n'));
|
|
277
|
+
|
|
278
|
+
// Resend setup
|
|
279
|
+
console.log(chalk.cyan('š Resend'));
|
|
280
|
+
const { resendApiKey, fromEmail } = await inquirer.prompt([
|
|
281
|
+
{
|
|
282
|
+
type: 'input',
|
|
283
|
+
name: 'resendApiKey',
|
|
284
|
+
message: 'Resend API Key (re_...):',
|
|
285
|
+
validate: (input) => {
|
|
286
|
+
if (!input) return 'Resend API Key is required';
|
|
287
|
+
if (!input.startsWith('re_')) return 'Must start with re_';
|
|
288
|
+
return true;
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
type: 'input',
|
|
293
|
+
name: 'fromEmail',
|
|
294
|
+
message: 'From Email (optional):',
|
|
295
|
+
default: 'onboarding@resend.dev',
|
|
296
|
+
},
|
|
297
|
+
]);
|
|
298
|
+
console.log(chalk.gray('š” Get from: resend.com/api-keys\n'));
|
|
299
|
+
|
|
300
|
+
// OAuth setup (optional)
|
|
301
|
+
console.log(chalk.cyan('š OAuth (Optional - press Enter to skip)'));
|
|
302
|
+
const { googleClientId, googleClientSecret, githubClientId, githubClientSecret } = await inquirer.prompt([
|
|
303
|
+
{
|
|
304
|
+
type: 'input',
|
|
305
|
+
name: 'googleClientId',
|
|
306
|
+
message: 'Google Client ID (optional):',
|
|
307
|
+
default: '',
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
type: 'input',
|
|
311
|
+
name: 'googleClientSecret',
|
|
312
|
+
message: 'Google Client Secret (optional):',
|
|
313
|
+
default: '',
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
type: 'input',
|
|
317
|
+
name: 'githubClientId',
|
|
318
|
+
message: 'GitHub Client ID (optional):',
|
|
319
|
+
default: '',
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
type: 'input',
|
|
323
|
+
name: 'githubClientSecret',
|
|
324
|
+
message: 'GitHub Client Secret (optional):',
|
|
325
|
+
default: '',
|
|
326
|
+
},
|
|
327
|
+
]);
|
|
328
|
+
console.log(chalk.gray('š” Google: console.cloud.google.com/apis/credentials'));
|
|
329
|
+
console.log(chalk.gray('š” GitHub: github.com/settings/developers\n'));
|
|
330
|
+
|
|
331
|
+
// Write to .env file
|
|
332
|
+
const fs = await import('fs-extra');
|
|
333
|
+
const envPath = path.join(projectPath, '.env');
|
|
334
|
+
|
|
335
|
+
const envContent = `# Database
|
|
336
|
+
DATABASE_URL="${databaseUrl}"
|
|
337
|
+
|
|
338
|
+
# NextAuth
|
|
339
|
+
NEXTAUTH_SECRET="${nextauthSecret}"
|
|
340
|
+
NEXTAUTH_URL="${nextauthUrl}"
|
|
341
|
+
|
|
342
|
+
# Stripe
|
|
343
|
+
STRIPE_SECRET_KEY="${stripeSecretKey}"
|
|
344
|
+
STRIPE_WEBHOOK_SECRET="whsec_..." # Get this from Stripe webhook settings
|
|
345
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="${stripePublishableKey}"
|
|
346
|
+
STRIPE_PRO_PRICE_ID="${stripeProPrice}"
|
|
347
|
+
STRIPE_ENTERPRISE_PRICE_ID="${stripeEnterprisePrice}"
|
|
348
|
+
|
|
349
|
+
# Resend
|
|
350
|
+
RESEND_API_KEY="${resendApiKey}"
|
|
351
|
+
FROM_EMAIL="${fromEmail}"
|
|
352
|
+
|
|
353
|
+
# OAuth Providers${googleClientId ? `
|
|
354
|
+
GOOGLE_CLIENT_ID="${googleClientId}"
|
|
355
|
+
GOOGLE_CLIENT_SECRET="${googleClientSecret}"` : `
|
|
356
|
+
# GOOGLE_CLIENT_ID=""
|
|
357
|
+
# GOOGLE_CLIENT_SECRET=""`}${githubClientId ? `
|
|
358
|
+
GITHUB_CLIENT_ID="${githubClientId}"
|
|
359
|
+
GITHUB_CLIENT_SECRET="${githubClientSecret}"` : `
|
|
360
|
+
# GITHUB_CLIENT_ID=""
|
|
361
|
+
# GITHUB_CLIENT_SECRET=""`}
|
|
362
|
+
`;
|
|
363
|
+
|
|
364
|
+
await fs.writeFile(envPath, envContent);
|
|
365
|
+
|
|
366
|
+
console.log(chalk.green('ā
Environment configured!'));
|
|
367
|
+
console.log(chalk.green('ā
Wrote to .env file\n'));
|
|
368
|
+
|
|
369
|
+
console.log(chalk.cyan("š Don't forget:"));
|
|
370
|
+
console.log(chalk.white(' 1. Get your Stripe Webhook Secret from Stripe Dashboard'));
|
|
371
|
+
console.log(chalk.white(' 2. Run: npm run db:push (to set up database)'));
|
|
372
|
+
console.log(chalk.white(' 3. Run: npm run db:seed (to add test users)'));
|
|
373
|
+
console.log(chalk.white(' 4. Run: npm run dev (to start development)\n'));
|
|
374
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import ejs from 'ejs';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { dirname } from 'path';
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
+
const __dirname = dirname(__filename);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Ensures a directory exists, creating it if necessary
|
|
12
|
+
* @param {string} dirPath - Directory path to ensure
|
|
13
|
+
*/
|
|
14
|
+
export async function ensureDir(dirPath) {
|
|
15
|
+
await fs.ensureDir(dirPath);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Writes content to a file, creating parent directories if needed
|
|
20
|
+
* @param {string} filePath - File path to write to
|
|
21
|
+
* @param {string} content - Content to write
|
|
22
|
+
*/
|
|
23
|
+
export async function writeFile(filePath, content) {
|
|
24
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
25
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Renders an EJS template with data
|
|
30
|
+
* @param {string} templatePath - Path to the EJS template file
|
|
31
|
+
* @param {object} data - Data to pass to the template
|
|
32
|
+
* @returns {Promise<string>} - Rendered template content
|
|
33
|
+
*/
|
|
34
|
+
export async function renderTemplate(templatePath, data = {}) {
|
|
35
|
+
const templateContent = await fs.readFile(templatePath, 'utf-8');
|
|
36
|
+
return ejs.render(templateContent, data);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Renders an EJS template and writes it to a file
|
|
41
|
+
* @param {string} templatePath - Path to the EJS template file
|
|
42
|
+
* @param {string} outputPath - Path where the rendered file should be written
|
|
43
|
+
* @param {object} data - Data to pass to the template
|
|
44
|
+
*/
|
|
45
|
+
export async function renderTemplateToFile(templatePath, outputPath, data = {}) {
|
|
46
|
+
const content = await renderTemplate(templatePath, data);
|
|
47
|
+
await writeFile(outputPath, content);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Copies a file or directory
|
|
52
|
+
* @param {string} source - Source path
|
|
53
|
+
* @param {string} destination - Destination path
|
|
54
|
+
*/
|
|
55
|
+
export async function copy(source, destination) {
|
|
56
|
+
await fs.copy(source, destination);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Gets the path to a template file
|
|
61
|
+
* @param {string} relativePath - Relative path within templates directory
|
|
62
|
+
* @returns {string} - Absolute path to template
|
|
63
|
+
*/
|
|
64
|
+
export function getTemplatePath(relativePath) {
|
|
65
|
+
// Go up from utils -> src -> root, then into src/templates
|
|
66
|
+
return path.join(__dirname, '..', 'templates', relativePath);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Checks if a file or directory exists
|
|
71
|
+
* @param {string} filePath - Path to check
|
|
72
|
+
* @returns {Promise<boolean>} - True if exists
|
|
73
|
+
*/
|
|
74
|
+
export async function exists(filePath) {
|
|
75
|
+
try {
|
|
76
|
+
await fs.access(filePath);
|
|
77
|
+
return true;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
package/src/utils/git.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Initializes a git repository in the specified directory
|
|
7
|
+
* @param {string} projectPath - Path to the project directory
|
|
8
|
+
* @returns {Promise<void>}
|
|
9
|
+
*/
|
|
10
|
+
export async function initGit(projectPath) {
|
|
11
|
+
try {
|
|
12
|
+
// Check if git is available
|
|
13
|
+
await execa('git', ['--version']);
|
|
14
|
+
} catch {
|
|
15
|
+
throw new Error('Git is not installed. Please install git to use this feature.');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Initialize git repository
|
|
19
|
+
await execa('git', ['init'], { cwd: projectPath });
|
|
20
|
+
|
|
21
|
+
// Create .gitignore if it doesn't exist
|
|
22
|
+
const gitignorePath = path.join(projectPath, '.gitignore');
|
|
23
|
+
if (!await fs.pathExists(gitignorePath)) {
|
|
24
|
+
const gitignoreContent = `# dependencies
|
|
25
|
+
node_modules
|
|
26
|
+
.pnp
|
|
27
|
+
.pnp.js
|
|
28
|
+
|
|
29
|
+
# testing
|
|
30
|
+
coverage
|
|
31
|
+
|
|
32
|
+
# next.js
|
|
33
|
+
.next/
|
|
34
|
+
out/
|
|
35
|
+
build
|
|
36
|
+
dist
|
|
37
|
+
|
|
38
|
+
# misc
|
|
39
|
+
.DS_Store
|
|
40
|
+
*.pem
|
|
41
|
+
|
|
42
|
+
# debug
|
|
43
|
+
npm-debug.log*
|
|
44
|
+
yarn-debug.log*
|
|
45
|
+
yarn-error.log*
|
|
46
|
+
|
|
47
|
+
# local env files
|
|
48
|
+
.env
|
|
49
|
+
.env*.local
|
|
50
|
+
|
|
51
|
+
# vercel
|
|
52
|
+
.vercel
|
|
53
|
+
|
|
54
|
+
# typescript
|
|
55
|
+
*.tsbuildinfo
|
|
56
|
+
next-env.d.ts
|
|
57
|
+
|
|
58
|
+
# prisma
|
|
59
|
+
prisma/migrations/
|
|
60
|
+
`;
|
|
61
|
+
await fs.writeFile(gitignorePath, gitignoreContent, 'utf-8');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Stage all files
|
|
65
|
+
await execa('git', ['add', '-A'], { cwd: projectPath });
|
|
66
|
+
|
|
67
|
+
// Create initial commit
|
|
68
|
+
await execa('git', ['commit', '-m', 'Initial commit from create-solostack'], { cwd: projectPath });
|
|
69
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Prints a success message with next steps after project generation
|
|
5
|
+
* @param {string} projectName - Name of the generated project
|
|
6
|
+
* @param {string} projectPath - Absolute path to the project
|
|
7
|
+
*/
|
|
8
|
+
export function printSuccess(projectName, projectPath) {
|
|
9
|
+
console.log(chalk.green('\nā
Success! Your SaaS boilerplate is ready.\n'));
|
|
10
|
+
|
|
11
|
+
console.log(chalk.cyan('š Project created at:'));
|
|
12
|
+
console.log(chalk.white(` ${projectPath}\n`));
|
|
13
|
+
|
|
14
|
+
console.log(chalk.cyan('š Next steps:\n'));
|
|
15
|
+
console.log(chalk.white(` cd ${projectName}`));
|
|
16
|
+
console.log(chalk.white(' cp .env.example .env'));
|
|
17
|
+
console.log(chalk.white(' # Add your environment variables to .env\n'));
|
|
18
|
+
|
|
19
|
+
console.log(chalk.cyan('šļø Database setup:\n'));
|
|
20
|
+
console.log(chalk.white(' npm run db:push # Push schema to database'));
|
|
21
|
+
console.log(chalk.white(' npm run db:seed # Seed with sample data\n'));
|
|
22
|
+
|
|
23
|
+
console.log(chalk.cyan('ā” Start development:\n'));
|
|
24
|
+
console.log(chalk.white(' npm run dev\n'));
|
|
25
|
+
|
|
26
|
+
console.log(chalk.cyan('š Documentation:\n'));
|
|
27
|
+
console.log(chalk.white(' - NextAuth: https://next-auth.js.org'));
|
|
28
|
+
console.log(chalk.white(' - Prisma: https://prisma.io'));
|
|
29
|
+
console.log(chalk.white(' - Stripe: https://stripe.com/docs'));
|
|
30
|
+
console.log(chalk.white(' - Resend: https://resend.com/docs\n'));
|
|
31
|
+
|
|
32
|
+
console.log(chalk.yellow('ā ļø Remember to:'));
|
|
33
|
+
console.log(chalk.white(' - Set up a PostgreSQL database'));
|
|
34
|
+
console.log(chalk.white(' - Get API keys for Stripe and Resend'));
|
|
35
|
+
console.log(chalk.white(' - Configure OAuth providers (optional)\n'));
|
|
36
|
+
|
|
37
|
+
console.log(chalk.magenta('š” Happy building! š\n'));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Prints an error message in red
|
|
42
|
+
* @param {string} message - Error message to display
|
|
43
|
+
*/
|
|
44
|
+
export function printError(message) {
|
|
45
|
+
console.error(chalk.red(`\nā Error: ${message}\n`));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Prints a warning message in yellow
|
|
50
|
+
* @param {string} message - Warning message to display
|
|
51
|
+
*/
|
|
52
|
+
export function printWarning(message) {
|
|
53
|
+
console.warn(chalk.yellow(`\nā ļø Warning: ${message}\n`));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Prints an info message in blue
|
|
58
|
+
* @param {string} message - Info message to display
|
|
59
|
+
*/
|
|
60
|
+
export function printInfo(message) {
|
|
61
|
+
console.log(chalk.blue(`\nā¹ļø ${message}\n`));
|
|
62
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Detects which package manager is available
|
|
7
|
+
* Priority: pnpm > yarn > npm
|
|
8
|
+
* @returns {Promise<{name: string, command: string}>} - Package manager info
|
|
9
|
+
*/
|
|
10
|
+
export async function detectPackageManager() {
|
|
11
|
+
try {
|
|
12
|
+
await execa('pnpm', ['--version']);
|
|
13
|
+
return { name: 'pnpm', command: 'pnpm' };
|
|
14
|
+
} catch {
|
|
15
|
+
try {
|
|
16
|
+
await execa('yarn', ['--version']);
|
|
17
|
+
return { name: 'yarn', command: 'yarn' };
|
|
18
|
+
} catch {
|
|
19
|
+
return { name: 'npm', command: 'npm' };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Installs npm packages in the specified directory
|
|
26
|
+
* @param {string} projectPath - Path to the project directory
|
|
27
|
+
* @returns {Promise<void>}
|
|
28
|
+
*/
|
|
29
|
+
export async function installPackages(projectPath) {
|
|
30
|
+
const pm = await detectPackageManager();
|
|
31
|
+
|
|
32
|
+
// Check if package.json exists
|
|
33
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
34
|
+
if (!await fs.pathExists(packageJsonPath)) {
|
|
35
|
+
throw new Error('package.json not found in project directory');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Install dependencies
|
|
39
|
+
const installCommand = pm.name === 'yarn' ? 'install' : 'install';
|
|
40
|
+
|
|
41
|
+
await execa(pm.command, [installCommand], {
|
|
42
|
+
cwd: projectPath,
|
|
43
|
+
stdio: 'inherit',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Adds a package to the project
|
|
49
|
+
* @param {string} projectPath - Path to the project directory
|
|
50
|
+
* @param {string|string[]} packages - Package name(s) to add
|
|
51
|
+
* @param {object} options - Options
|
|
52
|
+
* @param {boolean} options.dev - Install as dev dependency
|
|
53
|
+
*/
|
|
54
|
+
export async function addPackage(projectPath, packages, options = {}) {
|
|
55
|
+
const pm = await detectPackageManager();
|
|
56
|
+
const pkgArray = Array.isArray(packages) ? packages : [packages];
|
|
57
|
+
|
|
58
|
+
let args = [];
|
|
59
|
+
|
|
60
|
+
if (pm.name === 'npm') {
|
|
61
|
+
args = ['install', ...pkgArray];
|
|
62
|
+
if (options.dev) args.push('--save-dev');
|
|
63
|
+
} else if (pm.name === 'yarn') {
|
|
64
|
+
args = ['add', ...pkgArray];
|
|
65
|
+
if (options.dev) args.push('--dev');
|
|
66
|
+
} else if (pm.name === 'pnpm') {
|
|
67
|
+
args = ['add', ...pkgArray];
|
|
68
|
+
if (options.dev) args.push('--save-dev');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
await execa(pm.command, args, {
|
|
72
|
+
cwd: projectPath,
|
|
73
|
+
stdio: 'inherit',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import validateNpmPackageName from 'validate-npm-package-name';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validates a project name for npm package compatibility
|
|
5
|
+
* @param {string} name - The project name to validate
|
|
6
|
+
* @returns {boolean|string} - True if valid, error message if invalid
|
|
7
|
+
*/
|
|
8
|
+
export function validateProjectName(name) {
|
|
9
|
+
const validation = validateNpmPackageName(name);
|
|
10
|
+
|
|
11
|
+
if (!validation.validForNewPackages) {
|
|
12
|
+
const error = validation.errors?.[0] || validation.warnings?.[0] || 'Unknown error';
|
|
13
|
+
return `Invalid project name: ${error}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return true;
|
|
17
|
+
}
|