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/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
+ }
@@ -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
+ }