nextforge-cli 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/README.md +187 -0
- package/bin/cli.js +666 -0
- package/package.json +45 -0
- package/templates/auth/ldap/ldap-service.ts +146 -0
- package/templates/base/app/dashboard/page.tsx +97 -0
- package/templates/base/app/globals.css +59 -0
- package/templates/base/app/layout.tsx +27 -0
- package/templates/base/app/login/page.tsx +118 -0
- package/templates/base/app/page.tsx +48 -0
- package/templates/base/app/register/page.tsx +164 -0
- package/templates/base/components/ui/button.tsx +52 -0
- package/templates/base/components/ui/card.tsx +78 -0
- package/templates/base/components/ui/input.tsx +24 -0
- package/templates/base/components/ui/label.tsx +23 -0
- package/templates/base/features/auth/server/route.ts +160 -0
- package/templates/base/index.ts +75 -0
- package/templates/base/lib/auth.ts +74 -0
- package/templates/base/lib/logger.ts +66 -0
- package/templates/base/lib/prisma.ts +15 -0
- package/templates/base/lib/rpc.ts +35 -0
- package/templates/base/lib/store.ts +53 -0
- package/templates/base/lib/utils.ts +6 -0
- package/templates/base/middleware/auth-middleware.ts +42 -0
- package/templates/base/next.config.js +10 -0
- package/templates/base/postcss.config.js +6 -0
- package/templates/base/providers/providers.tsx +32 -0
- package/templates/base/tailwind.config.ts +58 -0
- package/templates/base/tsconfig.json +26 -0
- package/templates/email/email-service.ts +120 -0
- package/templates/stripe/route.ts +147 -0
- package/templates/stripe/stripe-service.ts +117 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,666 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import inquirer from 'inquirer';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import ora from 'ora';
|
|
7
|
+
import fs from 'fs-extra';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { execSync } from 'child_process';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
|
|
15
|
+
|
|
16
|
+
const program = new Command();
|
|
17
|
+
|
|
18
|
+
program
|
|
19
|
+
.name('nextforge')
|
|
20
|
+
.description('Forge your next full-stack Next.js TypeScript app')
|
|
21
|
+
.version('1.0.0')
|
|
22
|
+
.argument('[project-name]', 'Name of the project')
|
|
23
|
+
.action(async (projectName) => {
|
|
24
|
+
console.log(chalk.bold.cyan('\n🔥 NextForge - Full-Stack Next.js Generator\n'));
|
|
25
|
+
|
|
26
|
+
// Get project name if not provided
|
|
27
|
+
if (!projectName) {
|
|
28
|
+
const { name } = await inquirer.prompt([
|
|
29
|
+
{
|
|
30
|
+
type: 'input',
|
|
31
|
+
name: 'name',
|
|
32
|
+
message: 'Project name:',
|
|
33
|
+
default: 'my-fullstack-app',
|
|
34
|
+
validate: (input) => input.length > 0 || 'Project name is required',
|
|
35
|
+
},
|
|
36
|
+
]);
|
|
37
|
+
projectName = name;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Main configuration questions
|
|
41
|
+
const answers = await inquirer.prompt([
|
|
42
|
+
{
|
|
43
|
+
type: 'list',
|
|
44
|
+
name: 'database',
|
|
45
|
+
message: 'Which database do you want to use?',
|
|
46
|
+
choices: [
|
|
47
|
+
{ name: 'PostgreSQL', value: 'postgresql' },
|
|
48
|
+
{ name: 'MySQL', value: 'mysql' },
|
|
49
|
+
{ name: 'SQL Server', value: 'sqlserver' },
|
|
50
|
+
{ name: 'SQLite (dev only)', value: 'sqlite' },
|
|
51
|
+
],
|
|
52
|
+
default: 'postgresql',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
type: 'confirm',
|
|
56
|
+
name: 'useAuth',
|
|
57
|
+
message: 'Do you need authentication?',
|
|
58
|
+
default: true,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
type: 'list',
|
|
62
|
+
name: 'authType',
|
|
63
|
+
message: 'Which authentication type?',
|
|
64
|
+
choices: [
|
|
65
|
+
{ name: 'Credentials (email/password)', value: 'credentials' },
|
|
66
|
+
{ name: 'LDAP / Active Directory', value: 'ldap' },
|
|
67
|
+
{ name: 'OAuth (Google, GitHub, etc.)', value: 'oauth' },
|
|
68
|
+
{ name: 'All of the above', value: 'all' },
|
|
69
|
+
],
|
|
70
|
+
when: (ans) => ans.useAuth,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
type: 'confirm',
|
|
74
|
+
name: 'useStripe',
|
|
75
|
+
message: 'Do you need Stripe integration?',
|
|
76
|
+
default: false,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
type: 'confirm',
|
|
80
|
+
name: 'useEmail',
|
|
81
|
+
message: 'Do you need email functionality (nodemailer)?',
|
|
82
|
+
default: true,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
type: 'confirm',
|
|
86
|
+
name: 'useBullMQ',
|
|
87
|
+
message: 'Do you need background jobs (BullMQ + Redis)?',
|
|
88
|
+
default: true,
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
type: 'confirm',
|
|
92
|
+
name: 'useDocker',
|
|
93
|
+
message: 'Generate Docker & docker-compose files?',
|
|
94
|
+
default: true,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
type: 'list',
|
|
98
|
+
name: 'packageManager',
|
|
99
|
+
message: 'Which package manager?',
|
|
100
|
+
choices: ['npm', 'yarn', 'pnpm'],
|
|
101
|
+
default: 'npm',
|
|
102
|
+
},
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
const projectPath = path.join(process.cwd(), projectName);
|
|
106
|
+
|
|
107
|
+
// Check if directory exists
|
|
108
|
+
if (fs.existsSync(projectPath)) {
|
|
109
|
+
const { overwrite } = await inquirer.prompt([
|
|
110
|
+
{
|
|
111
|
+
type: 'confirm',
|
|
112
|
+
name: 'overwrite',
|
|
113
|
+
message: `Directory ${projectName} already exists. Overwrite?`,
|
|
114
|
+
default: false,
|
|
115
|
+
},
|
|
116
|
+
]);
|
|
117
|
+
if (!overwrite) {
|
|
118
|
+
console.log(chalk.yellow('Aborted.'));
|
|
119
|
+
process.exit(0);
|
|
120
|
+
}
|
|
121
|
+
fs.removeSync(projectPath);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const spinner = ora('Creating project structure...').start();
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
// Create project directory
|
|
128
|
+
fs.mkdirSync(projectPath, { recursive: true });
|
|
129
|
+
|
|
130
|
+
// Copy base template
|
|
131
|
+
spinner.text = 'Copying base template...';
|
|
132
|
+
fs.copySync(path.join(TEMPLATES_DIR, 'base'), projectPath);
|
|
133
|
+
|
|
134
|
+
// Copy auth templates if needed
|
|
135
|
+
if (answers.useAuth) {
|
|
136
|
+
spinner.text = 'Setting up authentication...';
|
|
137
|
+
const authTypes = answers.authType === 'all'
|
|
138
|
+
? ['credentials', 'ldap', 'oauth']
|
|
139
|
+
: [answers.authType];
|
|
140
|
+
|
|
141
|
+
for (const authType of authTypes) {
|
|
142
|
+
const authTemplatePath = path.join(TEMPLATES_DIR, 'auth', authType);
|
|
143
|
+
if (fs.existsSync(authTemplatePath)) {
|
|
144
|
+
fs.copySync(authTemplatePath, path.join(projectPath, 'features', 'auth', authType), { overwrite: true });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Copy Stripe template if needed
|
|
150
|
+
if (answers.useStripe) {
|
|
151
|
+
spinner.text = 'Setting up Stripe...';
|
|
152
|
+
const stripeTemplatePath = path.join(TEMPLATES_DIR, 'stripe');
|
|
153
|
+
if (fs.existsSync(stripeTemplatePath)) {
|
|
154
|
+
fs.copySync(stripeTemplatePath, path.join(projectPath, 'features', 'stripe'), { overwrite: true });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Copy email template if needed
|
|
159
|
+
if (answers.useEmail) {
|
|
160
|
+
spinner.text = 'Setting up email...';
|
|
161
|
+
const emailTemplatePath = path.join(TEMPLATES_DIR, 'email');
|
|
162
|
+
if (fs.existsSync(emailTemplatePath)) {
|
|
163
|
+
fs.copySync(emailTemplatePath, path.join(projectPath, 'lib', 'email'), { overwrite: true });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Generate package.json with selected dependencies
|
|
168
|
+
spinner.text = 'Generating package.json...';
|
|
169
|
+
const packageJson = generatePackageJson(projectName, answers);
|
|
170
|
+
fs.writeFileSync(
|
|
171
|
+
path.join(projectPath, 'package.json'),
|
|
172
|
+
JSON.stringify(packageJson, null, 2)
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Generate .env.example
|
|
176
|
+
spinner.text = 'Generating environment files...';
|
|
177
|
+
const envContent = generateEnvFile(answers);
|
|
178
|
+
fs.writeFileSync(path.join(projectPath, '.env.example'), envContent);
|
|
179
|
+
fs.writeFileSync(path.join(projectPath, '.env'), envContent);
|
|
180
|
+
|
|
181
|
+
// Generate Prisma schema
|
|
182
|
+
spinner.text = 'Generating Prisma schema...';
|
|
183
|
+
const prismaSchema = generatePrismaSchema(answers);
|
|
184
|
+
fs.mkdirSync(path.join(projectPath, 'prisma'), { recursive: true });
|
|
185
|
+
fs.writeFileSync(path.join(projectPath, 'prisma', 'schema.prisma'), prismaSchema);
|
|
186
|
+
|
|
187
|
+
// Generate Docker files if needed
|
|
188
|
+
if (answers.useDocker) {
|
|
189
|
+
spinner.text = 'Generating Docker files...';
|
|
190
|
+
const dockerCompose = generateDockerCompose(answers);
|
|
191
|
+
const dockerfile = generateDockerfile();
|
|
192
|
+
fs.writeFileSync(path.join(projectPath, 'docker-compose.yml'), dockerCompose);
|
|
193
|
+
fs.writeFileSync(path.join(projectPath, 'Dockerfile'), dockerfile);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Update tsconfig paths
|
|
197
|
+
spinner.text = 'Configuring TypeScript...';
|
|
198
|
+
|
|
199
|
+
spinner.succeed(chalk.green('Project created successfully!'));
|
|
200
|
+
|
|
201
|
+
// Print next steps
|
|
202
|
+
console.log(chalk.bold('\n📋 Next steps:\n'));
|
|
203
|
+
console.log(chalk.cyan(` cd ${projectName}`));
|
|
204
|
+
|
|
205
|
+
const installCmd = {
|
|
206
|
+
npm: 'npm install',
|
|
207
|
+
yarn: 'yarn',
|
|
208
|
+
pnpm: 'pnpm install',
|
|
209
|
+
}[answers.packageManager];
|
|
210
|
+
|
|
211
|
+
console.log(chalk.cyan(` ${installCmd}`));
|
|
212
|
+
console.log(chalk.cyan(' cp .env.example .env # Edit with your values'));
|
|
213
|
+
console.log(chalk.cyan(' npx prisma generate'));
|
|
214
|
+
console.log(chalk.cyan(' npx prisma db push'));
|
|
215
|
+
|
|
216
|
+
const devCmd = {
|
|
217
|
+
npm: 'npm run dev',
|
|
218
|
+
yarn: 'yarn dev',
|
|
219
|
+
pnpm: 'pnpm dev',
|
|
220
|
+
}[answers.packageManager];
|
|
221
|
+
|
|
222
|
+
console.log(chalk.cyan(` ${devCmd}`));
|
|
223
|
+
|
|
224
|
+
console.log(chalk.bold('\n🎉 Happy coding!\n'));
|
|
225
|
+
|
|
226
|
+
} catch (error) {
|
|
227
|
+
spinner.fail(chalk.red('Failed to create project'));
|
|
228
|
+
console.error(error);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
function generatePackageJson(projectName, answers) {
|
|
234
|
+
const pkg = {
|
|
235
|
+
name: projectName,
|
|
236
|
+
version: '0.1.0',
|
|
237
|
+
private: true,
|
|
238
|
+
type: 'module',
|
|
239
|
+
scripts: {
|
|
240
|
+
dev: 'concurrently "npm run dev:api" "npm run dev:ui"',
|
|
241
|
+
'dev:api': 'tsx watch index.ts',
|
|
242
|
+
'dev:ui': 'next dev -p 3001',
|
|
243
|
+
build: 'next build',
|
|
244
|
+
'build:api': 'tsc --project tsconfig.build.json',
|
|
245
|
+
start: 'concurrently "npm run start:api" "next start -p 3001"',
|
|
246
|
+
'start:api': 'node dist/index.js',
|
|
247
|
+
lint: 'next lint',
|
|
248
|
+
'db:push': 'prisma db push',
|
|
249
|
+
'db:generate': 'prisma generate',
|
|
250
|
+
'db:studio': 'prisma studio',
|
|
251
|
+
},
|
|
252
|
+
dependencies: {
|
|
253
|
+
// Core
|
|
254
|
+
next: '^14.2.22',
|
|
255
|
+
react: '^18.3.1',
|
|
256
|
+
'react-dom': '^18.3.1',
|
|
257
|
+
typescript: '^5.3.3',
|
|
258
|
+
|
|
259
|
+
// API
|
|
260
|
+
hono: '^4.0.0',
|
|
261
|
+
'@hono/node-server': '^1.8.2',
|
|
262
|
+
'@hono/zod-validator': '^0.2.1',
|
|
263
|
+
zod: '^3.23.8',
|
|
264
|
+
|
|
265
|
+
// Database
|
|
266
|
+
'@prisma/client': '^6.19.0',
|
|
267
|
+
prisma: '^6.19.0',
|
|
268
|
+
|
|
269
|
+
// State & Data Fetching
|
|
270
|
+
'@tanstack/react-query': '^5.60.2',
|
|
271
|
+
zustand: '^5.0.3',
|
|
272
|
+
|
|
273
|
+
// UI Components
|
|
274
|
+
'@radix-ui/react-checkbox': '^1.3.3',
|
|
275
|
+
'@radix-ui/react-dialog': '^1.1.15',
|
|
276
|
+
'@radix-ui/react-dropdown-menu': '^2.1.2',
|
|
277
|
+
'@radix-ui/react-label': '^2.1.8',
|
|
278
|
+
'@radix-ui/react-popover': '^1.1.15',
|
|
279
|
+
'@radix-ui/react-select': '^2.2.6',
|
|
280
|
+
'@radix-ui/react-slot': '^1.2.4',
|
|
281
|
+
'@radix-ui/react-tabs': '^1.1.13',
|
|
282
|
+
'@radix-ui/react-toast': '^1.2.2',
|
|
283
|
+
'lucide-react': '^0.446.0',
|
|
284
|
+
'class-variance-authority': '^0.7.0',
|
|
285
|
+
clsx: '^2.1.1',
|
|
286
|
+
'tailwind-merge': '^2.5.2',
|
|
287
|
+
'tailwindcss-animate': '^1.0.7',
|
|
288
|
+
'next-themes': '^0.4.6',
|
|
289
|
+
|
|
290
|
+
// Forms
|
|
291
|
+
'react-hook-form': '^7.53.2',
|
|
292
|
+
'@hookform/resolvers': '^3.9.1',
|
|
293
|
+
|
|
294
|
+
// Charts
|
|
295
|
+
recharts: '^3.3.0',
|
|
296
|
+
|
|
297
|
+
// Utilities
|
|
298
|
+
'date-fns': '^4.1.0',
|
|
299
|
+
dotenv: '^16.4.5',
|
|
300
|
+
|
|
301
|
+
// Logging
|
|
302
|
+
winston: '^3.18.3',
|
|
303
|
+
'winston-daily-rotate-file': '^5.0.0',
|
|
304
|
+
|
|
305
|
+
// Styling
|
|
306
|
+
tailwindcss: '^3.4.1',
|
|
307
|
+
postcss: '^8.4.35',
|
|
308
|
+
autoprefixer: '^10.4.17',
|
|
309
|
+
},
|
|
310
|
+
devDependencies: {
|
|
311
|
+
'@types/node': '^20.11.19',
|
|
312
|
+
'@types/react': '^18.3.11',
|
|
313
|
+
'@types/react-dom': '^18.3.0',
|
|
314
|
+
eslint: '^8.57.1',
|
|
315
|
+
'eslint-config-next': '^14.2.22',
|
|
316
|
+
tsx: '^4.7.1',
|
|
317
|
+
concurrently: '^9.0.1',
|
|
318
|
+
},
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// Add auth dependencies
|
|
322
|
+
if (answers.useAuth) {
|
|
323
|
+
pkg.dependencies['jsonwebtoken'] = '^9.0.2';
|
|
324
|
+
pkg.dependencies['bcrypt'] = '^5.1.1';
|
|
325
|
+
pkg.devDependencies['@types/jsonwebtoken'] = '^9.0.10';
|
|
326
|
+
pkg.devDependencies['@types/bcrypt'] = '^5.0.2';
|
|
327
|
+
|
|
328
|
+
if (answers.authType === 'ldap' || answers.authType === 'all') {
|
|
329
|
+
pkg.dependencies['ldapjs'] = '^3.0.7';
|
|
330
|
+
pkg.devDependencies['@types/ldapjs'] = '^2.2.5';
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Add Stripe
|
|
335
|
+
if (answers.useStripe) {
|
|
336
|
+
pkg.dependencies['stripe'] = '^14.0.0';
|
|
337
|
+
pkg.dependencies['@stripe/stripe-js'] = '^2.0.0';
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Add Email
|
|
341
|
+
if (answers.useEmail) {
|
|
342
|
+
pkg.dependencies['nodemailer'] = '^6.10.1';
|
|
343
|
+
pkg.devDependencies['@types/nodemailer'] = '^6.4.21';
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Add BullMQ
|
|
347
|
+
if (answers.useBullMQ) {
|
|
348
|
+
pkg.dependencies['bullmq'] = '^5.64.1';
|
|
349
|
+
pkg.dependencies['ioredis'] = '^5.8.2';
|
|
350
|
+
pkg.scripts['dev:worker'] = 'tsx watch workers/queue-worker.ts';
|
|
351
|
+
pkg.scripts['dev:full'] = 'concurrently "npm run dev:api" "npm run dev:ui" "npm run dev:worker"';
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return pkg;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function generateEnvFile(answers) {
|
|
358
|
+
let env = `# App Configuration
|
|
359
|
+
NODE_ENV=development
|
|
360
|
+
APP_NAME=My App
|
|
361
|
+
APP_URL=http://localhost:3001
|
|
362
|
+
|
|
363
|
+
# API Configuration
|
|
364
|
+
API_PORT=3000
|
|
365
|
+
NEXT_PUBLIC_API_URL=http://localhost:3000
|
|
366
|
+
|
|
367
|
+
# JWT Configuration
|
|
368
|
+
JWT_SECRET=your-super-secret-jwt-key-change-in-production
|
|
369
|
+
JWT_EXPIRES_IN=7d
|
|
370
|
+
|
|
371
|
+
`;
|
|
372
|
+
|
|
373
|
+
// Database
|
|
374
|
+
const dbUrls = {
|
|
375
|
+
postgresql: 'DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"',
|
|
376
|
+
mysql: 'DATABASE_URL="mysql://user:password@localhost:3306/mydb"',
|
|
377
|
+
sqlserver: 'DATABASE_URL="sqlserver://localhost:1433;database=mydb;user=sa;password=YourPassword;encrypt=true;trustServerCertificate=true"',
|
|
378
|
+
sqlite: 'DATABASE_URL="file:./dev.db"',
|
|
379
|
+
};
|
|
380
|
+
env += `# Database\n${dbUrls[answers.database]}\n\n`;
|
|
381
|
+
|
|
382
|
+
// Auth
|
|
383
|
+
if (answers.useAuth && (answers.authType === 'ldap' || answers.authType === 'all')) {
|
|
384
|
+
env += `# LDAP Configuration
|
|
385
|
+
LDAP_URL=ldap://your-ldap-server:389
|
|
386
|
+
LDAP_BIND_DN=CN=Service Account,OU=Users,DC=company,DC=com
|
|
387
|
+
LDAP_BIND_PASSWORD=your-bind-password
|
|
388
|
+
LDAP_SEARCH_BASE=DC=company,DC=com
|
|
389
|
+
LDAP_SEARCH_FILTER=(sAMAccountName={{username}})
|
|
390
|
+
|
|
391
|
+
`;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (answers.useAuth && (answers.authType === 'oauth' || answers.authType === 'all')) {
|
|
395
|
+
env += `# OAuth Configuration
|
|
396
|
+
GOOGLE_CLIENT_ID=your-google-client-id
|
|
397
|
+
GOOGLE_CLIENT_SECRET=your-google-client-secret
|
|
398
|
+
GITHUB_CLIENT_ID=your-github-client-id
|
|
399
|
+
GITHUB_CLIENT_SECRET=your-github-client-secret
|
|
400
|
+
|
|
401
|
+
`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Stripe
|
|
405
|
+
if (answers.useStripe) {
|
|
406
|
+
env += `# Stripe Configuration
|
|
407
|
+
STRIPE_SECRET_KEY=sk_test_your-secret-key
|
|
408
|
+
STRIPE_PUBLISHABLE_KEY=pk_test_your-publishable-key
|
|
409
|
+
STRIPE_WEBHOOK_SECRET=whsec_your-webhook-secret
|
|
410
|
+
|
|
411
|
+
`;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Email
|
|
415
|
+
if (answers.useEmail) {
|
|
416
|
+
env += `# Email Configuration (SMTP)
|
|
417
|
+
SMTP_HOST=smtp.gmail.com
|
|
418
|
+
SMTP_PORT=587
|
|
419
|
+
SMTP_SECURE=false
|
|
420
|
+
SMTP_USER=your-email@gmail.com
|
|
421
|
+
SMTP_PASSWORD=your-app-password
|
|
422
|
+
EMAIL_FROM=noreply@yourapp.com
|
|
423
|
+
EMAIL_FROM_NAME=Your App
|
|
424
|
+
|
|
425
|
+
`;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// BullMQ
|
|
429
|
+
if (answers.useBullMQ) {
|
|
430
|
+
env += `# Redis Configuration (for BullMQ)
|
|
431
|
+
REDIS_HOST=localhost
|
|
432
|
+
REDIS_PORT=6379
|
|
433
|
+
REDIS_PASSWORD=
|
|
434
|
+
|
|
435
|
+
`;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return env;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function generatePrismaSchema(answers) {
|
|
442
|
+
const providers = {
|
|
443
|
+
postgresql: 'postgresql',
|
|
444
|
+
mysql: 'mysql',
|
|
445
|
+
sqlserver: 'sqlserver',
|
|
446
|
+
sqlite: 'sqlite',
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
let schema = `generator client {
|
|
450
|
+
provider = "prisma-client-js"
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
datasource db {
|
|
454
|
+
provider = "${providers[answers.database]}"
|
|
455
|
+
url = env("DATABASE_URL")
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
`;
|
|
459
|
+
|
|
460
|
+
// User model for auth
|
|
461
|
+
if (answers.useAuth) {
|
|
462
|
+
schema += `model User {
|
|
463
|
+
id Int @id @default(autoincrement())
|
|
464
|
+
username String @unique
|
|
465
|
+
email String? @unique
|
|
466
|
+
password String?
|
|
467
|
+
displayName String?
|
|
468
|
+
role String @default("user")
|
|
469
|
+
ldapDN String?
|
|
470
|
+
createdAt DateTime @default(now())
|
|
471
|
+
updatedAt DateTime @updatedAt
|
|
472
|
+
`;
|
|
473
|
+
|
|
474
|
+
if (answers.useStripe) {
|
|
475
|
+
schema += ` stripeCustomerId String?
|
|
476
|
+
subscriptions Subscription[]
|
|
477
|
+
`;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
schema += `}
|
|
481
|
+
|
|
482
|
+
`;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Stripe models
|
|
486
|
+
if (answers.useStripe) {
|
|
487
|
+
schema += `model Subscription {
|
|
488
|
+
id Int @id @default(autoincrement())
|
|
489
|
+
userId Int
|
|
490
|
+
stripeSubscriptionId String @unique
|
|
491
|
+
status String
|
|
492
|
+
priceId String
|
|
493
|
+
currentPeriodStart DateTime
|
|
494
|
+
currentPeriodEnd DateTime
|
|
495
|
+
createdAt DateTime @default(now())
|
|
496
|
+
updatedAt DateTime @updatedAt
|
|
497
|
+
|
|
498
|
+
user User @relation(fields: [userId], references: [id])
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
`;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return schema;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function generateDockerCompose(answers) {
|
|
508
|
+
let compose = `services:
|
|
509
|
+
`;
|
|
510
|
+
|
|
511
|
+
// Database
|
|
512
|
+
if (answers.database === 'postgresql') {
|
|
513
|
+
compose += ` database:
|
|
514
|
+
image: postgres:16-alpine
|
|
515
|
+
container_name: app-database
|
|
516
|
+
environment:
|
|
517
|
+
POSTGRES_USER: user
|
|
518
|
+
POSTGRES_PASSWORD: password
|
|
519
|
+
POSTGRES_DB: mydb
|
|
520
|
+
ports:
|
|
521
|
+
- "5432:5432"
|
|
522
|
+
volumes:
|
|
523
|
+
- postgres-data:/var/lib/postgresql/data
|
|
524
|
+
restart: unless-stopped
|
|
525
|
+
|
|
526
|
+
`;
|
|
527
|
+
} else if (answers.database === 'mysql') {
|
|
528
|
+
compose += ` database:
|
|
529
|
+
image: mysql:8
|
|
530
|
+
container_name: app-database
|
|
531
|
+
environment:
|
|
532
|
+
MYSQL_ROOT_PASSWORD: rootpassword
|
|
533
|
+
MYSQL_DATABASE: mydb
|
|
534
|
+
MYSQL_USER: user
|
|
535
|
+
MYSQL_PASSWORD: password
|
|
536
|
+
ports:
|
|
537
|
+
- "3306:3306"
|
|
538
|
+
volumes:
|
|
539
|
+
- mysql-data:/var/lib/mysql
|
|
540
|
+
restart: unless-stopped
|
|
541
|
+
|
|
542
|
+
`;
|
|
543
|
+
} else if (answers.database === 'sqlserver') {
|
|
544
|
+
compose += ` database:
|
|
545
|
+
image: mcr.microsoft.com/mssql/server:2022-latest
|
|
546
|
+
container_name: app-database
|
|
547
|
+
environment:
|
|
548
|
+
- ACCEPT_EULA=Y
|
|
549
|
+
- MSSQL_SA_PASSWORD=YourStrong@Passw0rd
|
|
550
|
+
- MSSQL_PID=Express
|
|
551
|
+
ports:
|
|
552
|
+
- "1433:1433"
|
|
553
|
+
volumes:
|
|
554
|
+
- sqlserver-data:/var/opt/mssql
|
|
555
|
+
restart: unless-stopped
|
|
556
|
+
|
|
557
|
+
`;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Redis for BullMQ
|
|
561
|
+
if (answers.useBullMQ) {
|
|
562
|
+
compose += ` redis:
|
|
563
|
+
image: redis:7-alpine
|
|
564
|
+
container_name: app-redis
|
|
565
|
+
ports:
|
|
566
|
+
- "6379:6379"
|
|
567
|
+
volumes:
|
|
568
|
+
- redis-data:/data
|
|
569
|
+
restart: unless-stopped
|
|
570
|
+
|
|
571
|
+
`;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// App
|
|
575
|
+
compose += ` webapp:
|
|
576
|
+
build:
|
|
577
|
+
context: .
|
|
578
|
+
dockerfile: Dockerfile
|
|
579
|
+
container_name: app-webapp
|
|
580
|
+
ports:
|
|
581
|
+
- "3000:3000"
|
|
582
|
+
- "3001:3001"
|
|
583
|
+
environment:
|
|
584
|
+
- NODE_ENV=production
|
|
585
|
+
depends_on:
|
|
586
|
+
`;
|
|
587
|
+
|
|
588
|
+
if (answers.database !== 'sqlite') {
|
|
589
|
+
compose += ` - database
|
|
590
|
+
`;
|
|
591
|
+
}
|
|
592
|
+
if (answers.useBullMQ) {
|
|
593
|
+
compose += ` - redis
|
|
594
|
+
`;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
compose += ` restart: unless-stopped
|
|
598
|
+
|
|
599
|
+
`;
|
|
600
|
+
|
|
601
|
+
// Volumes
|
|
602
|
+
compose += `volumes:
|
|
603
|
+
`;
|
|
604
|
+
if (answers.database === 'postgresql') {
|
|
605
|
+
compose += ` postgres-data:
|
|
606
|
+
`;
|
|
607
|
+
} else if (answers.database === 'mysql') {
|
|
608
|
+
compose += ` mysql-data:
|
|
609
|
+
`;
|
|
610
|
+
} else if (answers.database === 'sqlserver') {
|
|
611
|
+
compose += ` sqlserver-data:
|
|
612
|
+
`;
|
|
613
|
+
}
|
|
614
|
+
if (answers.useBullMQ) {
|
|
615
|
+
compose += ` redis-data:
|
|
616
|
+
`;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return compose;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function generateDockerfile() {
|
|
623
|
+
return `FROM node:20-alpine AS base
|
|
624
|
+
|
|
625
|
+
# Install dependencies only when needed
|
|
626
|
+
FROM base AS deps
|
|
627
|
+
RUN apk add --no-cache libc6-compat
|
|
628
|
+
WORKDIR /app
|
|
629
|
+
|
|
630
|
+
COPY package*.json ./
|
|
631
|
+
RUN npm ci
|
|
632
|
+
|
|
633
|
+
# Rebuild the source code only when needed
|
|
634
|
+
FROM base AS builder
|
|
635
|
+
WORKDIR /app
|
|
636
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
637
|
+
COPY . .
|
|
638
|
+
|
|
639
|
+
RUN npx prisma generate
|
|
640
|
+
RUN npm run build
|
|
641
|
+
|
|
642
|
+
# Production image
|
|
643
|
+
FROM base AS runner
|
|
644
|
+
WORKDIR /app
|
|
645
|
+
|
|
646
|
+
ENV NODE_ENV=production
|
|
647
|
+
|
|
648
|
+
RUN addgroup --system --gid 1001 nodejs
|
|
649
|
+
RUN adduser --system --uid 1001 nextjs
|
|
650
|
+
|
|
651
|
+
COPY --from=builder /app/public ./public
|
|
652
|
+
COPY --from=builder /app/.next/standalone ./
|
|
653
|
+
COPY --from=builder /app/.next/static ./.next/static
|
|
654
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
655
|
+
COPY --from=builder /app/dist ./dist
|
|
656
|
+
COPY --from=builder /app/prisma ./prisma
|
|
657
|
+
|
|
658
|
+
USER nextjs
|
|
659
|
+
|
|
660
|
+
EXPOSE 3000 3001
|
|
661
|
+
|
|
662
|
+
CMD ["node", "server.js"]
|
|
663
|
+
`;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nextforge-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Forge your next full-stack Next.js app with Hono, Prisma, TanStack Query, Zustand, BullMQ and more",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nextforge": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "node bin/cli.js",
|
|
11
|
+
"build": "echo 'No build needed'",
|
|
12
|
+
"test": "node bin/cli.js --help"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"nextjs",
|
|
16
|
+
"next",
|
|
17
|
+
"typescript",
|
|
18
|
+
"boilerplate",
|
|
19
|
+
"hono",
|
|
20
|
+
"prisma",
|
|
21
|
+
"bullmq",
|
|
22
|
+
"fullstack",
|
|
23
|
+
"tanstack",
|
|
24
|
+
"zustand",
|
|
25
|
+
"tailwind",
|
|
26
|
+
"shadcn",
|
|
27
|
+
"cli",
|
|
28
|
+
"scaffold",
|
|
29
|
+
"starter",
|
|
30
|
+
"template"
|
|
31
|
+
],
|
|
32
|
+
"author": "Brandon",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/BrandonDev12345/nextforge-cli"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"chalk": "^5.3.0",
|
|
40
|
+
"commander": "^12.1.0",
|
|
41
|
+
"inquirer": "^9.2.23",
|
|
42
|
+
"ora": "^8.0.1",
|
|
43
|
+
"fs-extra": "^11.2.0"
|
|
44
|
+
}
|
|
45
|
+
}
|