create-express-kickstart 1.3.2 → 1.3.4

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/bin/cli.js CHANGED
@@ -1,393 +1,804 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from 'fs';
4
- import path from 'path';
5
- import { execSync } from 'child_process';
6
- import { fileURLToPath } from 'url';
7
- import readline from 'readline';
8
-
9
- const __filename = fileURLToPath(import.meta.url);
10
- const __dirname = path.dirname(__filename);
11
-
12
- const rl = readline.createInterface({
13
- input: process.stdin,
14
- output: process.stdout
15
- });
16
-
17
- const question = (query) => new Promise((resolve) => rl.question(query, resolve));
18
-
19
- async function init() {
20
- const projectNameArg = process.argv[2];
21
-
22
- let projectName = projectNameArg;
23
- if (!projectName) {
24
- projectName = await question('\n> Project Directory Name (e.g. my-awesome-api): ');
25
- }
26
-
27
- if (!projectName) {
28
- console.error('\nError: Project Directory Name is required.');
29
- process.exit(1);
30
- }
31
-
32
- const currentPath = process.cwd();
33
- const projectPath = path.join(currentPath, projectName);
34
-
35
- if (fs.existsSync(projectPath)) {
36
- console.error(`\nError: Folder ${projectName} already exists. Please choose a different directory name.\n`);
37
- process.exit(1);
38
- }
39
-
40
- let packageJsonName = await question(`> package.json name (${projectName}): `);
41
- if (!packageJsonName.trim()) {
42
- packageJsonName = projectName; // Fallback to directory name
43
- }
44
-
45
- const description = await question('> Project description: ');
46
- const author = await question('> Author name: ');
47
-
48
- console.log('\n--- Select Dependencies ---');
49
- console.log('Press Enter for Yes (Y), type "n" for No.\n');
50
-
51
- const deps = {
52
- express: true, // Always required
53
- mongoose: (await question('Include Mongoose (MongoDB)? [Y/n] ')).toLowerCase() !== 'n',
54
- cors: (await question('Include CORS? [Y/n] ')).toLowerCase() !== 'n',
55
- helmet: (await question('Include Helmet (Security headers)? [Y/n] ')).toLowerCase() !== 'n',
56
- 'cookie-parser': (await question('Include cookie-parser? [Y/n] ')).toLowerCase() !== 'n',
57
- 'pino-http': (await question('Include Pino (HTTP Logger)? [Y/n] ')).toLowerCase() !== 'n',
58
- 'express-rate-limit': (await question('Include Rate Limiting? [Y/n] ')).toLowerCase() !== 'n',
59
- dotenv: (await question('Include dotenvx (Environment variables)? [Y/n] ')).toLowerCase() !== 'n',
60
- prettier: (await question('Include Prettier (Code formatter)? [Y/n] ')).toLowerCase() !== 'n'
61
- };
62
-
63
- let installPinoPretty = false;
64
- if (deps['pino-http']) {
65
- installPinoPretty = (await question('Include pino-pretty for clean development logs? [Y/n] ')).toLowerCase() !== 'n';
66
- }
67
-
68
- const packageManagerChoice = await question('\n> Which package manager would you like to use? [npm/yarn/pnpm/bun] (default: npm): ');
69
- const packageManager = ['yarn', 'pnpm', 'bun'].includes(packageManagerChoice.trim().toLowerCase())
70
- ? packageManagerChoice.trim().toLowerCase()
71
- : 'npm';
72
-
73
- const initGit = (await question('\n> Initialize a git repository? [Y/n] ')).toLowerCase() !== 'n';
74
- const initDocker = (await question('> Include Dockerfile & docker-compose.yml? [Y/n] ')).toLowerCase() !== 'n';
75
- const initAuth = (await question('> Include basic JWT Auth boilerplate? [Y/n] ')).toLowerCase() !== 'n';
76
- const initTests = (await question('> Include Jest setup and boilerplate tests? [Y/n] ')).toLowerCase() !== 'n';
77
-
78
- rl.close();
79
-
80
- console.log(`\n Creating a new Node.js Express API in ${projectPath}...`);
81
- fs.mkdirSync(projectPath, { recursive: true });
82
-
83
- function copyRecursiveSync(src, dest) {
84
- const exists = fs.existsSync(src);
85
- const stats = exists && fs.statSync(src);
86
- const isDirectory = exists && stats.isDirectory();
87
- if (isDirectory) {
88
- fs.mkdirSync(dest, { recursive: true });
89
- fs.readdirSync(src).forEach((childItemName) => {
90
- copyRecursiveSync(path.join(src, childItemName), path.join(dest, childItemName));
91
- });
92
- } else {
93
- fs.copyFileSync(src, dest);
94
- }
95
- }
96
-
97
- // 1. Copy src directory
98
- const sourceDir = path.join(__dirname, '..', 'src');
99
- const targetSrcDir = path.join(projectPath, 'src');
100
-
101
- if (!fs.existsSync(sourceDir)) {
102
- console.error('\nError: Could not find "src" directory in the template generator.');
103
- process.exit(1);
104
- }
105
-
106
- console.log(` Bootstrapping application structure (errorHandler, ApiResponse, async handlers)...`);
107
- copyRecursiveSync(sourceDir, targetSrcDir);
108
-
109
- // 2. Copy .env.example
110
- console.log(` Generating environment files...`);
111
- const envExamplePath = path.join(__dirname, '..', '.env.example');
112
- if (fs.existsSync(envExamplePath)) {
113
- fs.copyFileSync(envExamplePath, path.join(projectPath, '.env.example'));
114
- fs.copyFileSync(envExamplePath, path.join(projectPath, '.env.local'));
115
- }
116
-
117
- if (initDocker) {
118
- console.log(` Adding Docker files...`);
119
- const dockerfilePath = path.join(__dirname, '..', 'templates', 'Dockerfile');
120
- const dockerComposePath = path.join(__dirname, '..', 'templates', 'docker-compose.yml');
121
-
122
- // Fallbacks if templates aren't bundled right
123
- if (fs.existsSync(dockerfilePath)) fs.copyFileSync(dockerfilePath, path.join(projectPath, 'Dockerfile'));
124
- if (fs.existsSync(dockerComposePath) && deps.mongoose) {
125
- fs.copyFileSync(dockerComposePath, path.join(projectPath, 'docker-compose.yml'));
126
- }
127
- }
128
-
129
- if (initAuth) {
130
- console.log(` Adding Auth templates...`);
131
- // Need to ensure directories exist
132
- fs.mkdirSync(path.join(projectPath, 'src', 'controllers'), { recursive: true });
133
- fs.mkdirSync(path.join(projectPath, 'src', 'middlewares'), { recursive: true });
134
- fs.mkdirSync(path.join(projectPath, 'src', 'routes'), { recursive: true });
135
-
136
- // Copy the templates
137
- fs.copyFileSync(
138
- path.join(__dirname, '..', 'templates', 'auth', 'auth.controller.js'),
139
- path.join(projectPath, 'src', 'controllers', 'auth.controller.js')
140
- );
141
- fs.copyFileSync(
142
- path.join(__dirname, '..', 'templates', 'auth', 'auth.middleware.js'),
143
- path.join(projectPath, 'src', 'middlewares', 'auth.middleware.js')
144
- );
145
- fs.copyFileSync(
146
- path.join(__dirname, '..', 'templates', 'auth', 'auth.routes.js'),
147
- path.join(projectPath, 'src', 'routes', 'auth.routes.js')
148
- );
149
-
150
- // Append JWT secret and Salt Rounds to env example
151
- const authEnvConfig = `
152
- # Bcrypt Configuration
153
- BCRYPT_SALT=10
154
-
155
- # JWT Configuration
156
- JWT_SECRET=supersecretjwtkey123
157
- JWT_EXPIRES_IN=1d
158
- `;
159
- fs.appendFileSync(path.join(projectPath, '.env.example'), authEnvConfig);
160
- fs.appendFileSync(path.join(projectPath, '.env.local'), authEnvConfig);
161
-
162
- const utilsPath = path.join(projectPath, 'src', 'utils');
163
- if (!fs.existsSync(utilsPath)) {
164
- fs.mkdirSync(utilsPath, { recursive: true });
165
- }
166
- fs.writeFileSync(
167
- path.join(utilsPath, 'hash.util.js'),
168
- `import bcrypt from "bcryptjs";
169
-
170
- export const hashData = async (data, saltRounds = process.env.BCRYPT_SALT) => {
171
- const salt = await bcrypt.genSalt(Number(saltRounds) || 10);
172
- return await bcrypt.hash(data, salt);
173
- };
174
-
175
- export const compareData = async (data, hashedData) => {
176
- return await bcrypt.compare(data, hashedData);
177
- };
178
- `
179
- );
180
-
181
- fs.writeFileSync(
182
- path.join(utilsPath, 'jwt.util.js'),
183
- `import jwt from "jsonwebtoken";
184
-
185
- export const generateToken = (payload, expiresIn = process.env.JWT_EXPIRES_IN || "1d") => {
186
- return jwt.sign(payload, process.env.JWT_SECRET, { expiresIn });
187
- };
188
-
189
- export const verifyToken = (token) => {
190
- return jwt.verify(token, process.env.JWT_SECRET);
191
- };
192
- `
193
- );
194
-
195
- }
196
-
197
- if (initTests) {
198
- console.log(` Adding Jest test templates...`);
199
- fs.mkdirSync(path.join(projectPath, 'tests'), { recursive: true });
200
- fs.copyFileSync(
201
- path.join(__dirname, '..', 'templates', 'tests', 'healthcheck.test.js'),
202
- path.join(projectPath, 'tests', 'healthcheck.test.js')
203
- );
204
- }
205
-
206
- // Rewrite app.js and server.js based on selections
207
- let appJsPath = path.join(projectPath, 'src', 'app.js');
208
- if (fs.existsSync(appJsPath)) {
209
- let appJsCode = fs.readFileSync(appJsPath, 'utf8');
210
-
211
- if (initAuth) {
212
- appJsCode = appJsCode.replace(
213
- '// Import routers',
214
- '// Import routers\nimport authRouter from "#routes/auth.routes.js";'
215
- );
216
- appJsCode = appJsCode.replace(
217
- '// Mount routers',
218
- '// Mount routers\napp.use("/api/v1/auth", authRouter);'
219
- );
220
- }
221
- if (!deps.cors) {
222
- appJsCode = appJsCode.replace(/import cors from "cors";\r?\n/, '');
223
- appJsCode = appJsCode.replace(/\/\/ CORS setup[\s\S]*?\n\);\r?\n/, '');
224
- }
225
- if (!deps.helmet) {
226
- appJsCode = appJsCode.replace(/import helmet from "helmet";\r?\n/, '');
227
- appJsCode = appJsCode.replace(/\/\/ Security HTTP headers\r?\napp\.use\(helmet\(\)\);\r?\n/, '');
228
- }
229
- if (!deps['cookie-parser']) {
230
- appJsCode = appJsCode.replace(/import cookieParser from "cookie-parser";\r?\n/, '');
231
- appJsCode = appJsCode.replace(/app\.use\(cookieParser\(\)\);\r?\n/, '');
232
- }
233
- if (!deps['pino-http']) {
234
- appJsCode = appJsCode.replace(/import pinoHttp from "pino-http";\r?\n/, '');
235
- appJsCode = appJsCode.replace(/\/\/ Logging[\s\S]*?\}\)\(\) \? : undefined\n\}\)\);\r?\n/g, ''); // Fallback block
236
- appJsCode = appJsCode.replace(/\/\/ Logging[\s\S]*?\}\)\(\) : undefined\r?\n\}\)\);\r?\n/g, '');
237
- }
238
- if (!deps['express-rate-limit']) {
239
- appJsCode = appJsCode.replace(/import rateLimit from "express-rate-limit";\r?\n/, '');
240
- appJsCode = appJsCode.replace(/\/\/ Rate Limiting[\s\S]*?app\.use\("\/api", limiter\);[^\n]*\n/g, '');
241
- }
242
-
243
- fs.writeFileSync(appJsPath, appJsCode);
244
- }
245
-
246
- let serverJsPath = path.join(projectPath, 'src', 'server.js');
247
- if (fs.existsSync(serverJsPath)) {
248
- let serverJsCode = fs.readFileSync(serverJsPath, 'utf8');
249
-
250
- if (!deps.mongoose) {
251
- serverJsCode = serverJsCode.replace(/import connectDB from "#db\/index\.js";\r?\n/, '');
252
- serverJsCode = serverJsCode.replace(/connectDB\(\)\r?\n \.then\(\(\) => \{\r?\n/, '');
253
- serverJsCode = serverJsCode.replace(/ \}\)\r?\n \.catch\(\(err\) => \{\r?\n console\.log\("MONGO db connection failed !!! ", err\);\r?\n \}\);\r?\n/, '');
254
- // Fix indentation for app.listen
255
- serverJsCode = serverJsCode.replace(/ app\.listen\(PORT, \(\) => \{\r?\n console\.log\(`Server is running at port : \$\{PORT\}`\);\r?\n \}\);\r?\n/, 'app.listen(PORT, () => {\n console.log(`Server is running at port : ${PORT}`);\n});\n');
256
-
257
- const dbDir = path.join(projectPath, 'src', 'db');
258
- if (fs.existsSync(dbDir)) fs.rmSync(dbDir, { recursive: true, force: true });
259
- }
260
-
261
- fs.writeFileSync(serverJsPath, serverJsCode);
262
- }
263
-
264
- // 3. Create package.json
265
- console.log(` Setting up package.json...`);
266
- const packageJsonTemplate = {
267
- name: packageJsonName.trim(),
268
- version: "1.0.0",
269
- description: description || "A production-ready Node.js Express API",
270
- main: "src/server.js",
271
- type: "module",
272
- scripts: {
273
- "start": deps.dotenv ? "dotenvx run -f .env.local -- node src/server.js" : "node src/server.js",
274
- "dev": deps.dotenv ? "dotenvx run -f .env.local -- nodemon src/server.js" : "nodemon src/server.js"
275
- },
276
- imports: {
277
- "#*": "./src/*"
278
- },
279
- keywords: ["express", "node", "api"],
280
- author: author || "",
281
- license: "ISC"
282
- };
283
-
284
- if (deps.prettier) {
285
- packageJsonTemplate.scripts.format = "prettier --write \"src/**/*.{js,json}\"";
286
- }
287
-
288
- if (initTests) {
289
- packageJsonTemplate.scripts.test = "node --experimental-vm-modules node_modules/jest/bin/jest.js";
290
- }
291
-
292
- // Write package.json
293
- fs.writeFileSync(
294
- path.join(projectPath, 'package.json'),
295
- JSON.stringify(packageJsonTemplate, null, 2)
296
- );
297
-
298
- // Install Dependencies
299
- const dependenciesToInstall = Object.keys(deps).filter(dep => deps[dep] && dep !== 'prettier' && dep !== 'dotenv');
300
- if (deps['pino-http']) {
301
- dependenciesToInstall.push('pino');
302
- }
303
- if (initAuth) {
304
- dependenciesToInstall.push('jsonwebtoken', 'bcryptjs'); // Add bcryptjs too since it's standard with JWT
305
- }
306
-
307
- const devDependenciesToInstall = ['nodemon'];
308
- if (deps.dotenv) devDependenciesToInstall.push('@dotenvx/dotenvx');
309
- if (deps.prettier) devDependenciesToInstall.push('prettier');
310
- if (installPinoPretty) devDependenciesToInstall.push('pino-pretty');
311
- if (initTests) {
312
- devDependenciesToInstall.push('jest', 'supertest');
313
- }
314
-
315
- try {
316
- const execConfig = { cwd: projectPath, stdio: 'inherit' };
317
-
318
- // Inject dependencies directly into package.json instead of doing them via raw arguments.
319
- // This perfectly bypasses PNPM / YARN / BUN specific registry caching bugs when downloading deeply nested trees.
320
- console.log(`\n Configuring ${packageManager} and resolving dependency trees...`);
321
- const finalPackageJsonPath = path.join(projectPath, 'package.json');
322
- const finalPackageJsonCode = JSON.parse(fs.readFileSync(finalPackageJsonPath, 'utf8'));
323
-
324
- // We add them dynamically so package managers can evaluate them holistically at once
325
- const latestDeps = {};
326
- dependenciesToInstall.forEach(d => latestDeps[d] = 'latest');
327
- finalPackageJsonCode.dependencies = latestDeps;
328
-
329
- const latestDevDeps = {};
330
- devDependenciesToInstall.forEach(d => latestDevDeps[d] = 'latest');
331
- finalPackageJsonCode.devDependencies = latestDevDeps;
332
-
333
- fs.writeFileSync(finalPackageJsonPath, JSON.stringify(finalPackageJsonCode, null, 2));
334
-
335
- console.log(`\n Running final installation via ${packageManager} (This might take a minute)...`);
336
- const installTriggerCmd = packageManager === 'npm' ? 'npm install' : `${packageManager} install`;
337
- execSync(installTriggerCmd, execConfig);
338
-
339
- // Update package.json with the actual installed versions instead of "latest"
340
- try {
341
- const installedPackageJson = JSON.parse(fs.readFileSync(finalPackageJsonPath, 'utf8'));
342
-
343
- const getInstalledVersion = (dep) => {
344
- try {
345
- const depPkgPath = path.join(projectPath, 'node_modules', dep, 'package.json');
346
- const depPkgCode = JSON.parse(fs.readFileSync(depPkgPath, 'utf8'));
347
- return `^${depPkgCode.version}`;
348
- } catch (err) {
349
- return 'latest';
350
- }
351
- };
352
-
353
- dependenciesToInstall.forEach(d => {
354
- installedPackageJson.dependencies[d] = getInstalledVersion(d);
355
- });
356
-
357
- devDependenciesToInstall.forEach(d => {
358
- installedPackageJson.devDependencies[d] = getInstalledVersion(d);
359
- });
360
-
361
- fs.writeFileSync(finalPackageJsonPath, JSON.stringify(installedPackageJson, null, 2));
362
- } catch (err) {
363
- // Silently fall back to 'latest' if parsing fails
364
- }
365
-
366
- if (initGit) {
367
- console.log(`\n Initializing Git repository...`);
368
- execSync('git init', { cwd: projectPath, stdio: 'inherit' });
369
- // Create .gitignore
370
- const gitignoreContent = "node_modules\n.env\n.env.keys\n.env.local\ndist\nbuild\ncoverage\n";
371
- fs.writeFileSync(path.join(projectPath, '.gitignore'), gitignoreContent);
372
- execSync('git add .', { cwd: projectPath, stdio: 'inherit' });
373
- execSync('git commit -m "initial commit"', { cwd: projectPath, stdio: 'inherit' });
374
- }
375
-
376
- console.log(`\n Success! Created "${projectName}" at ${projectPath}`);
377
- console.log('\nInside that directory, you can run several commands:');
378
- console.log(`\n ${packageManager === 'npm' ? 'npm run' : packageManager} dev`);
379
- console.log(' Starts the development server on localhost.');
380
- console.log(`\n ${packageManager === 'npm' ? 'npm' : packageManager} start`);
381
- console.log(' Starts the production server.');
382
- console.log('\nWe suggest that you begin by typing:');
383
- console.log(`\n cd ${projectName}`);
384
- console.log(` ${packageManager === 'npm' ? 'npm run' : packageManager} dev\n`);
385
- } catch (err) {
386
- console.error('\nFailed to install dependencies. You may need to install them manually inside the folder.', err);
387
- }
388
- }
389
-
390
- init().catch(err => {
391
- console.error('\nUnexpected error occurred:', err);
392
- process.exit(1);
393
- });
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync } from "child_process";
4
+ import crypto from "crypto";
5
+ import fs from "fs";
6
+ import path from "path";
7
+ import { fileURLToPath } from "url";
8
+ import readline from "readline";
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+ const ROOT_DIR = path.join(__dirname, "..");
13
+ const DEFAULT_PORT = 8000;
14
+ const DEFAULT_PACKAGE_MANAGER = "npm";
15
+ const AUTH_SECRET_PLACEHOLDER = "replace-me-with-a-long-random-secret";
16
+ const SUPPORTED_PACKAGE_MANAGERS = new Set(["npm", "yarn", "pnpm", "bun"]);
17
+ const ENV_SKIP_INSTALL = "CREATE_EXPRESS_KICKSTART_SKIP_INSTALL";
18
+ const ENV_SKIP_GIT = "CREATE_EXPRESS_KICKSTART_SKIP_GIT";
19
+
20
+ const DEFAULT_DEPENDENCIES = {
21
+ express: true,
22
+ mongoose: true,
23
+ cors: true,
24
+ helmet: true,
25
+ "cookie-parser": true,
26
+ "pino-http": true,
27
+ "express-rate-limit": true,
28
+ dotenv: true,
29
+ prettier: true,
30
+ };
31
+
32
+ const GITIGNORE_CONTENT = `node_modules
33
+ .env
34
+ .env.keys
35
+ .env.local
36
+ dist
37
+ build
38
+ coverage
39
+ `;
40
+
41
+ const HASH_UTIL_TEMPLATE = `import bcrypt from "bcryptjs";
42
+
43
+ export const hashData = async (data, saltRounds = process.env.BCRYPT_SALT_ROUNDS) => {
44
+ const salt = await bcrypt.genSalt(Number(saltRounds) || 10);
45
+ return bcrypt.hash(data, salt);
46
+ };
47
+
48
+ export const compareData = async (data, hashedData) => {
49
+ return bcrypt.compare(data, hashedData);
50
+ };
51
+ `;
52
+
53
+ const JWT_UTIL_TEMPLATE = `import jwt from "jsonwebtoken";
54
+
55
+ const getJwtSecret = () => {
56
+ if (!process.env.JWT_SECRET) {
57
+ throw new Error("JWT_SECRET must be set before using JWT helpers.");
58
+ }
59
+
60
+ return process.env.JWT_SECRET;
61
+ };
62
+
63
+ export const generateToken = (payload, expiresIn = process.env.JWT_EXPIRES_IN || "1d") => {
64
+ return jwt.sign(payload, getJwtSecret(), { expiresIn });
65
+ };
66
+
67
+ export const verifyToken = (token) => {
68
+ return jwt.verify(token, getJwtSecret());
69
+ };
70
+ `;
71
+
72
+ const DOCKER_TEMPLATE_MAP = {
73
+ npm: {
74
+ baseImage: "node:22-alpine",
75
+ packageManagerSetup: "",
76
+ installCommand: "npm install --omit=dev",
77
+ runtime: "node",
78
+ },
79
+ yarn: {
80
+ baseImage: "node:22-alpine",
81
+ packageManagerSetup: "RUN corepack enable",
82
+ installCommand: "yarn install --production=true",
83
+ runtime: "node",
84
+ },
85
+ pnpm: {
86
+ baseImage: "node:22-alpine",
87
+ packageManagerSetup: "RUN corepack enable",
88
+ installCommand: "pnpm install --prod",
89
+ runtime: "node",
90
+ },
91
+ bun: {
92
+ baseImage: "oven/bun:1-alpine",
93
+ packageManagerSetup: "",
94
+ installCommand: "bun install --production",
95
+ runtime: "bun",
96
+ },
97
+ };
98
+
99
+ const parseYesNo = (answer) => answer.trim().toLowerCase() !== "n";
100
+
101
+ export const normalizePackageManager = (value) => {
102
+ const normalized = value.trim().toLowerCase();
103
+ return SUPPORTED_PACKAGE_MANAGERS.has(normalized)
104
+ ? normalized
105
+ : DEFAULT_PACKAGE_MANAGER;
106
+ };
107
+
108
+ const unique = (items) => [...new Set(items)];
109
+
110
+ const createSecret = () => crypto.randomBytes(32).toString("hex");
111
+
112
+ const copyRecursiveSync = (src, dest) => {
113
+ const stats = fs.statSync(src);
114
+
115
+ if (stats.isDirectory()) {
116
+ fs.mkdirSync(dest, { recursive: true });
117
+
118
+ for (const child of fs.readdirSync(src)) {
119
+ copyRecursiveSync(path.join(src, child), path.join(dest, child));
120
+ }
121
+
122
+ return;
123
+ }
124
+
125
+ fs.copyFileSync(src, dest);
126
+ };
127
+
128
+ const renderTemplate = (template, replacements) => {
129
+ let output = template;
130
+
131
+ for (const [token, value] of Object.entries(replacements)) {
132
+ output = output.replaceAll(token, value);
133
+ }
134
+
135
+ return output
136
+ .replace(/[ \t]+\n/g, "\n")
137
+ .replace(/\n{3,}/g, "\n\n")
138
+ .trimEnd()
139
+ .concat("\n");
140
+ };
141
+
142
+ const readTemplate = (...segments) =>
143
+ fs.readFileSync(path.join(ROOT_DIR, ...segments), "utf8");
144
+
145
+ const writeJson = (filePath, value) => {
146
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
147
+ };
148
+
149
+ const appendBlock = (filePath, block) => {
150
+ const currentValue = fs.readFileSync(filePath, "utf8").trimEnd();
151
+ fs.writeFileSync(filePath, `${currentValue}\n\n${block.trim()}\n`);
152
+ };
153
+
154
+ const createPackageJsonTemplate = (config) => {
155
+ const packageJsonTemplate = {
156
+ name: config.packageJsonName.trim(),
157
+ version: "1.0.0",
158
+ description: config.description || "A configurable Node.js Express API starter",
159
+ main: "src/server.js",
160
+ type: "module",
161
+ scripts: {
162
+ start: config.deps.dotenv
163
+ ? "dotenvx run -f .env.local -- node src/server.js"
164
+ : "node src/server.js",
165
+ dev: config.deps.dotenv
166
+ ? "dotenvx run -f .env.local -- nodemon src/server.js"
167
+ : "nodemon src/server.js",
168
+ },
169
+ imports: {
170
+ "#*": "./src/*",
171
+ },
172
+ keywords: ["express", "node", "api"],
173
+ author: config.author || "",
174
+ license: "ISC",
175
+ };
176
+
177
+ if (config.deps.prettier) {
178
+ packageJsonTemplate.scripts.format = 'prettier --write "src/**/*.{js,json}"';
179
+ }
180
+
181
+ if (config.initTests) {
182
+ packageJsonTemplate.scripts.test =
183
+ "node --experimental-vm-modules node_modules/jest/bin/jest.js";
184
+ }
185
+
186
+ return packageJsonTemplate;
187
+ };
188
+
189
+ const resolveDependencyLists = (config) => {
190
+ const dependencyCandidates = Object.entries(config.deps)
191
+ .filter(([dependencyName, enabled]) => {
192
+ return enabled && dependencyName !== "dotenv" && dependencyName !== "prettier";
193
+ })
194
+ .map(([dependencyName]) => dependencyName);
195
+
196
+ const dependencies = unique([
197
+ ...dependencyCandidates,
198
+ ...(config.deps["pino-http"] ? ["pino"] : []),
199
+ ...(config.initAuth ? ["jsonwebtoken", "bcryptjs"] : []),
200
+ ]);
201
+
202
+ const devDependencies = unique([
203
+ "nodemon",
204
+ ...(config.deps.dotenv ? ["@dotenvx/dotenvx"] : []),
205
+ ...(config.deps.prettier ? ["prettier"] : []),
206
+ ...(config.installPinoPretty && config.deps["pino-http"] ? ["pino-pretty"] : []),
207
+ ...(config.initTests ? ["jest", "supertest"] : []),
208
+ ]);
209
+
210
+ return { dependencies, devDependencies };
211
+ };
212
+
213
+ const updatePackageJsonDependencies = (projectPath, dependencies, devDependencies) => {
214
+ const packageJsonPath = path.join(projectPath, "package.json");
215
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
216
+
217
+ packageJson.dependencies = Object.fromEntries(
218
+ dependencies.map((dependencyName) => [dependencyName, "latest"]),
219
+ );
220
+ packageJson.devDependencies = Object.fromEntries(
221
+ devDependencies.map((dependencyName) => [dependencyName, "latest"]),
222
+ );
223
+
224
+ writeJson(packageJsonPath, packageJson);
225
+ };
226
+
227
+ const updatePackageJsonWithInstalledVersions = (projectPath, dependencies, devDependencies) => {
228
+ const packageJsonPath = path.join(projectPath, "package.json");
229
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
230
+
231
+ const getInstalledVersion = (dependencyName) => {
232
+ try {
233
+ const dependencyPackageJson = JSON.parse(
234
+ fs.readFileSync(
235
+ path.join(projectPath, "node_modules", dependencyName, "package.json"),
236
+ "utf8",
237
+ ),
238
+ );
239
+
240
+ return `^${dependencyPackageJson.version}`;
241
+ } catch {
242
+ return "latest";
243
+ }
244
+ };
245
+
246
+ for (const dependencyName of dependencies) {
247
+ packageJson.dependencies[dependencyName] = getInstalledVersion(dependencyName);
248
+ }
249
+
250
+ for (const dependencyName of devDependencies) {
251
+ packageJson.devDependencies[dependencyName] = getInstalledVersion(dependencyName);
252
+ }
253
+
254
+ writeJson(packageJsonPath, packageJson);
255
+ };
256
+
257
+ const buildDockerfile = (packageManager) => {
258
+ const dockerTemplate = readTemplate("templates", "Dockerfile");
259
+ const dockerOptions = DOCKER_TEMPLATE_MAP[packageManager] || DOCKER_TEMPLATE_MAP.npm;
260
+
261
+ return renderTemplate(dockerTemplate, {
262
+ "__BASE_IMAGE__": dockerOptions.baseImage,
263
+ "__PACKAGE_MANAGER_SETUP__": dockerOptions.packageManagerSetup,
264
+ "__INSTALL_COMMAND__": dockerOptions.installCommand,
265
+ "__PORT__": String(DEFAULT_PORT),
266
+ "__RUNTIME__": dockerOptions.runtime,
267
+ });
268
+ };
269
+
270
+ const buildDockerCompose = () => {
271
+ const dockerComposeTemplate = readTemplate("templates", "docker-compose.yml");
272
+
273
+ return renderTemplate(dockerComposeTemplate, {
274
+ "__PORT__": String(DEFAULT_PORT),
275
+ "__MONGODB_URI__": "mongodb://mongo:27017/my_app_db",
276
+ "__CORS_ORIGIN__": "http://localhost:3000",
277
+ });
278
+ };
279
+
280
+ const buildAppCode = (config) => {
281
+ const appTemplate = readTemplate("src", "app.js");
282
+
283
+ return renderTemplate(appTemplate, {
284
+ "__CORS_IMPORT__": config.deps.cors ? 'import cors from "cors";' : "",
285
+ "__COOKIE_PARSER_IMPORT__": config.deps["cookie-parser"]
286
+ ? 'import cookieParser from "cookie-parser";'
287
+ : "",
288
+ "__HELMET_IMPORT__": config.deps.helmet ? 'import helmet from "helmet";' : "",
289
+ "__LOGGER_IMPORT__": config.deps["pino-http"] ? 'import pinoHttp from "pino-http";' : "",
290
+ "__RATE_LIMIT_IMPORT__": config.deps["express-rate-limit"]
291
+ ? 'import rateLimit from "express-rate-limit";'
292
+ : "",
293
+ "__AUTH_IMPORT__": config.initAuth ? 'import authRouter from "#routes/auth.routes.js";' : "",
294
+ "__HELMET_SETUP__": config.deps.helmet ? "app.use(helmet());" : "",
295
+ "__RATE_LIMIT_SETUP__": config.deps["express-rate-limit"]
296
+ ? `const limiter = rateLimit({
297
+ windowMs: Number(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000,
298
+ limit: Number(process.env.RATE_LIMIT_MAX) || 100,
299
+ standardHeaders: "draft-7",
300
+ legacyHeaders: false,
301
+ message: "Too many requests from this IP, please try again later",
302
+ });
303
+
304
+ app.use("/api", limiter);`
305
+ : "",
306
+ "__LOGGER_SETUP__": config.deps["pino-http"]
307
+ ? `const enablePrettyLogs =
308
+ process.env.NODE_ENV === "development" && process.env.PINO_PRETTY === "true";
309
+
310
+ app.use(
311
+ pinoHttp({
312
+ customLogLevel(req, res, err) {
313
+ if (res.statusCode >= 500 || err) {
314
+ return "error";
315
+ }
316
+
317
+ if (res.statusCode >= 400) {
318
+ return "warn";
319
+ }
320
+
321
+ if (res.statusCode >= 300) {
322
+ return "silent";
323
+ }
324
+
325
+ return "info";
326
+ },
327
+ transport: enablePrettyLogs
328
+ ? {
329
+ target: "pino-pretty",
330
+ options: { colorize: true },
331
+ }
332
+ : undefined,
333
+ }),
334
+ );`
335
+ : "",
336
+ "__CORS_SETUP__": config.deps.cors
337
+ ? `const allowedOrigins = (process.env.CORS_ORIGIN || "")
338
+ .split(",")
339
+ .map((origin) => origin.trim())
340
+ .filter(Boolean);
341
+ const allowAllOrigins = allowedOrigins.includes("*");
342
+
343
+ if (process.env.NODE_ENV === "production" && allowedOrigins.length === 0) {
344
+ throw new Error("CORS_ORIGIN must list one or more allowed origins in production.");
345
+ }
346
+
347
+ app.use(
348
+ cors({
349
+ origin: allowAllOrigins
350
+ ? true
351
+ : allowedOrigins.length > 0
352
+ ? allowedOrigins
353
+ : true,
354
+ credentials: !allowAllOrigins && allowedOrigins.length > 0,
355
+ }),
356
+ );`
357
+ : "",
358
+ "__COOKIE_PARSER_SETUP__": config.deps["cookie-parser"] ? "app.use(cookieParser());" : "",
359
+ "__AUTH_ROUTE__": config.initAuth ? 'app.use("/api/v1/auth", authRouter);' : "",
360
+ });
361
+ };
362
+
363
+ const buildServerCode = (config) => {
364
+ const serverTemplate = readTemplate("src", "server.js");
365
+
366
+ return renderTemplate(serverTemplate, {
367
+ "__DB_IMPORT__": config.deps.mongoose ? 'import connectDB from "#db/index.js";' : "",
368
+ "__SERVER_STARTUP__": config.deps.mongoose
369
+ ? `const bootstrap = async () => {
370
+ await connectDB();
371
+ startServer();
372
+ };
373
+
374
+ bootstrap().catch((error) => {
375
+ console.error("Database connection failed", error);
376
+ process.exit(1);
377
+ });`
378
+ : "startServer();",
379
+ });
380
+ };
381
+
382
+ const ensureDir = (dirPath) => {
383
+ fs.mkdirSync(dirPath, { recursive: true });
384
+ };
385
+
386
+ const writeAuthUtilities = (projectPath) => {
387
+ const utilsPath = path.join(projectPath, "src", "utils");
388
+ ensureDir(utilsPath);
389
+
390
+ fs.writeFileSync(path.join(utilsPath, "hash.util.js"), HASH_UTIL_TEMPLATE);
391
+ fs.writeFileSync(path.join(utilsPath, "jwt.util.js"), JWT_UTIL_TEMPLATE);
392
+ };
393
+
394
+ const writeAuthFiles = (projectPath) => {
395
+ ensureDir(path.join(projectPath, "src", "controllers"));
396
+ ensureDir(path.join(projectPath, "src", "middlewares"));
397
+ ensureDir(path.join(projectPath, "src", "routes"));
398
+ ensureDir(path.join(projectPath, "src", "models"));
399
+
400
+ fs.copyFileSync(
401
+ path.join(ROOT_DIR, "templates", "auth", "auth.controller.js"),
402
+ path.join(projectPath, "src", "controllers", "auth.controller.js"),
403
+ );
404
+ fs.copyFileSync(
405
+ path.join(ROOT_DIR, "templates", "auth", "auth.middleware.js"),
406
+ path.join(projectPath, "src", "middlewares", "auth.middleware.js"),
407
+ );
408
+ fs.copyFileSync(
409
+ path.join(ROOT_DIR, "templates", "auth", "auth.routes.js"),
410
+ path.join(projectPath, "src", "routes", "auth.routes.js"),
411
+ );
412
+ fs.copyFileSync(
413
+ path.join(ROOT_DIR, "templates", "auth", "user.model.js"),
414
+ path.join(projectPath, "src", "models", "user.model.js"),
415
+ );
416
+ };
417
+
418
+ const addAuthEnvironment = (projectPath, secretGenerator) => {
419
+ appendBlock(
420
+ path.join(projectPath, ".env.example"),
421
+ `# Bcrypt Configuration
422
+ BCRYPT_SALT_ROUNDS=10
423
+
424
+ # JWT Configuration
425
+ JWT_SECRET=${AUTH_SECRET_PLACEHOLDER}
426
+ JWT_EXPIRES_IN=1d`,
427
+ );
428
+
429
+ appendBlock(
430
+ path.join(projectPath, ".env.local"),
431
+ `# Bcrypt Configuration
432
+ BCRYPT_SALT_ROUNDS=10
433
+
434
+ # JWT Configuration
435
+ JWT_SECRET=${secretGenerator()}
436
+ JWT_EXPIRES_IN=1d`,
437
+ );
438
+ };
439
+
440
+ const addPinoPrettyEnvironment = (projectPath) => {
441
+ appendBlock(path.join(projectPath, ".env.example"), "PINO_PRETTY=true");
442
+ appendBlock(path.join(projectPath, ".env.local"), "PINO_PRETTY=true");
443
+ };
444
+
445
+ const initializeGitRepository = ({ projectPath, runCommand, logger, skipGit }) => {
446
+ fs.writeFileSync(path.join(projectPath, ".gitignore"), GITIGNORE_CONTENT);
447
+
448
+ if (skipGit) {
449
+ logger.log(` Skipping git initialization because ${ENV_SKIP_GIT}=1.`);
450
+ return {
451
+ gitInitialized: false,
452
+ warnings: ["Git initialization was skipped by environment override."],
453
+ };
454
+ }
455
+
456
+ const warnings = [];
457
+
458
+ try {
459
+ runCommand("git init", { cwd: projectPath, stdio: "inherit" });
460
+ runCommand("git add .", { cwd: projectPath, stdio: "inherit" });
461
+ runCommand('git commit -m "initial commit"', {
462
+ cwd: projectPath,
463
+ stdio: "inherit",
464
+ });
465
+
466
+ return { gitInitialized: true, warnings };
467
+ } catch (error) {
468
+ warnings.push(
469
+ "Git initialization completed partially. Review git configuration before committing.",
470
+ );
471
+ logger.warn("\nGit setup could not finish cleanly. The project files are still ready to use.");
472
+
473
+ return { gitInitialized: false, warnings, error };
474
+ }
475
+ };
476
+
477
+ const installDependencies = ({
478
+ projectPath,
479
+ packageManager,
480
+ dependencies,
481
+ devDependencies,
482
+ runCommand,
483
+ logger,
484
+ skipInstall,
485
+ }) => {
486
+ updatePackageJsonDependencies(projectPath, dependencies, devDependencies);
487
+
488
+ if (skipInstall) {
489
+ logger.log(`\n Skipping dependency installation because ${ENV_SKIP_INSTALL}=1.`);
490
+ return {
491
+ installSucceeded: false,
492
+ warnings: ["Dependency installation was skipped by environment override."],
493
+ };
494
+ }
495
+
496
+ try {
497
+ logger.log(`\n Configuring ${packageManager} and resolving dependency trees...`);
498
+ logger.log(`\n Running final installation via ${packageManager} (this might take a minute)...`);
499
+
500
+ const installCommand =
501
+ packageManager === "npm" ? "npm install" : `${packageManager} install`;
502
+
503
+ runCommand(installCommand, { cwd: projectPath, stdio: "inherit" });
504
+ updatePackageJsonWithInstalledVersions(projectPath, dependencies, devDependencies);
505
+
506
+ return { installSucceeded: true, warnings: [] };
507
+ } catch (error) {
508
+ logger.warn(
509
+ "\nDependency installation did not complete. You can still open the project and run the install manually.",
510
+ );
511
+
512
+ return {
513
+ installSucceeded: false,
514
+ warnings: [
515
+ "Dependency installation failed. Run the package manager manually inside the project.",
516
+ ],
517
+ error,
518
+ };
519
+ }
520
+ };
521
+
522
+ export const createProject = (rawConfig, runtime = {}) => {
523
+ const logger = runtime.logger || console;
524
+ const cwd = runtime.cwd || process.cwd();
525
+ const runCommand = runtime.runCommand || execSync;
526
+ const secretGenerator = runtime.secretGenerator || createSecret;
527
+ const skipInstall = runtime.skipInstall ?? process.env[ENV_SKIP_INSTALL] === "1";
528
+ const skipGit = runtime.skipGit ?? process.env[ENV_SKIP_GIT] === "1";
529
+
530
+ const config = {
531
+ ...rawConfig,
532
+ projectName: rawConfig.projectName?.trim(),
533
+ packageJsonName: rawConfig.packageJsonName?.trim() || rawConfig.projectName?.trim(),
534
+ packageManager: normalizePackageManager(
535
+ rawConfig.packageManager || DEFAULT_PACKAGE_MANAGER,
536
+ ),
537
+ deps: {
538
+ ...DEFAULT_DEPENDENCIES,
539
+ ...rawConfig.deps,
540
+ express: true,
541
+ },
542
+ };
543
+
544
+ if (!config.projectName) {
545
+ throw new Error("Project directory name is required.");
546
+ }
547
+
548
+ if (!config.packageJsonName) {
549
+ throw new Error("package.json name is required.");
550
+ }
551
+
552
+ const warnings = [];
553
+ const projectPath = path.join(cwd, config.projectName);
554
+
555
+ if (fs.existsSync(projectPath)) {
556
+ throw new Error(
557
+ `Folder ${config.projectName} already exists. Please choose a different directory name.`,
558
+ );
559
+ }
560
+
561
+ if (config.initAuth && !config.deps.mongoose) {
562
+ config.deps.mongoose = true;
563
+ const authWarning =
564
+ "JWT auth boilerplate requires Mongoose in this starter, so MongoDB support was enabled automatically.";
565
+ warnings.push(authWarning);
566
+ logger.log(`\n ${authWarning}`);
567
+ }
568
+
569
+ logger.log(`\n Creating a new Node.js Express API in ${projectPath}...`);
570
+ fs.mkdirSync(projectPath, { recursive: true });
571
+
572
+ const sourceDir = path.join(ROOT_DIR, "src");
573
+ const targetSrcDir = path.join(projectPath, "src");
574
+
575
+ if (!fs.existsSync(sourceDir)) {
576
+ throw new Error('Could not find "src" directory in the template generator.');
577
+ }
578
+
579
+ logger.log(" Bootstrapping application structure...");
580
+ copyRecursiveSync(sourceDir, targetSrcDir);
581
+
582
+ logger.log(" Generating environment files...");
583
+ const envExamplePath = path.join(ROOT_DIR, ".env.example");
584
+ if (fs.existsSync(envExamplePath)) {
585
+ fs.copyFileSync(envExamplePath, path.join(projectPath, ".env.example"));
586
+ fs.copyFileSync(envExamplePath, path.join(projectPath, ".env.local"));
587
+ }
588
+
589
+ fs.writeFileSync(path.join(targetSrcDir, "app.js"), buildAppCode(config));
590
+ fs.writeFileSync(path.join(targetSrcDir, "server.js"), buildServerCode(config));
591
+
592
+ if (!config.deps.mongoose) {
593
+ const dbDir = path.join(targetSrcDir, "db");
594
+ if (fs.existsSync(dbDir)) {
595
+ fs.rmSync(dbDir, { recursive: true, force: true });
596
+ }
597
+ }
598
+
599
+ if (config.initDocker) {
600
+ logger.log(" Adding Docker files...");
601
+ fs.writeFileSync(path.join(projectPath, "Dockerfile"), buildDockerfile(config.packageManager));
602
+ fs.copyFileSync(
603
+ path.join(ROOT_DIR, "templates", ".dockerignore"),
604
+ path.join(projectPath, ".dockerignore"),
605
+ );
606
+
607
+ if (config.deps.mongoose) {
608
+ fs.writeFileSync(path.join(projectPath, "docker-compose.yml"), buildDockerCompose());
609
+ }
610
+ }
611
+
612
+ if (config.initAuth) {
613
+ logger.log(" Adding auth templates...");
614
+ writeAuthFiles(projectPath);
615
+ writeAuthUtilities(projectPath);
616
+ addAuthEnvironment(projectPath, secretGenerator);
617
+ }
618
+
619
+ if (config.installPinoPretty && config.deps["pino-http"]) {
620
+ addPinoPrettyEnvironment(projectPath);
621
+ }
622
+
623
+ if (config.initTests) {
624
+ logger.log(" Adding Jest test templates...");
625
+ ensureDir(path.join(projectPath, "tests"));
626
+ fs.copyFileSync(
627
+ path.join(ROOT_DIR, "templates", "tests", "healthcheck.test.js"),
628
+ path.join(projectPath, "tests", "healthcheck.test.js"),
629
+ );
630
+ }
631
+
632
+ logger.log(" Setting up package.json...");
633
+ writeJson(path.join(projectPath, "package.json"), createPackageJsonTemplate(config));
634
+
635
+ const { dependencies, devDependencies } = resolveDependencyLists(config);
636
+ const installResult = installDependencies({
637
+ projectPath,
638
+ packageManager: config.packageManager,
639
+ dependencies,
640
+ devDependencies,
641
+ runCommand,
642
+ logger,
643
+ skipInstall,
644
+ });
645
+ warnings.push(...installResult.warnings);
646
+
647
+ let gitResult = { gitInitialized: false, warnings: [] };
648
+ if (config.initGit) {
649
+ logger.log("\n Initializing Git repository...");
650
+ gitResult = initializeGitRepository({
651
+ projectPath,
652
+ runCommand,
653
+ logger,
654
+ skipGit,
655
+ });
656
+ warnings.push(...gitResult.warnings);
657
+ }
658
+
659
+ return {
660
+ projectPath,
661
+ config,
662
+ dependencies,
663
+ devDependencies,
664
+ installSucceeded: installResult.installSucceeded,
665
+ gitInitialized: gitResult.gitInitialized,
666
+ warnings,
667
+ };
668
+ };
669
+
670
+ const createQuestioner = () => {
671
+ const rl = readline.createInterface({
672
+ input: process.stdin,
673
+ output: process.stdout,
674
+ });
675
+
676
+ return {
677
+ ask(prompt) {
678
+ return new Promise((resolve) => rl.question(prompt, resolve));
679
+ },
680
+ close() {
681
+ rl.close();
682
+ },
683
+ };
684
+ };
685
+
686
+ export const runCli = async ({
687
+ argv = process.argv,
688
+ cwd = process.cwd(),
689
+ logger = console,
690
+ questioner = createQuestioner(),
691
+ } = {}) => {
692
+
693
+ try {
694
+ let projectName = argv[2];
695
+ if (!projectName) {
696
+ projectName = await questioner.ask("\n> Project Directory Name (e.g. my-awesome-api): ");
697
+ }
698
+
699
+ if (!projectName?.trim()) {
700
+ throw new Error("Project directory name is required.");
701
+ }
702
+
703
+ let packageJsonName = await questioner.ask(`> package.json name (${projectName}): `);
704
+ if (!packageJsonName.trim()) {
705
+ packageJsonName = projectName;
706
+ }
707
+
708
+ const description = await questioner.ask("> Project description: ");
709
+ const author = await questioner.ask("> Author name: ");
710
+
711
+ logger.log("\n--- Select Dependencies ---");
712
+ logger.log('Press Enter for Yes (Y), type "n" for No.\n');
713
+
714
+ const deps = {
715
+ express: true,
716
+ mongoose: parseYesNo(await questioner.ask("Include Mongoose (MongoDB)? [Y/n] ")),
717
+ cors: parseYesNo(await questioner.ask("Include CORS? [Y/n] ")),
718
+ helmet: parseYesNo(await questioner.ask("Include Helmet (Security headers)? [Y/n] ")),
719
+ "cookie-parser": parseYesNo(await questioner.ask("Include cookie-parser? [Y/n] ")),
720
+ "pino-http": parseYesNo(await questioner.ask("Include Pino (HTTP Logger)? [Y/n] ")),
721
+ "express-rate-limit": parseYesNo(
722
+ await questioner.ask("Include Rate Limiting? [Y/n] "),
723
+ ),
724
+ dotenv: parseYesNo(await questioner.ask("Include dotenvx (Environment variables)? [Y/n] ")),
725
+ prettier: parseYesNo(await questioner.ask("Include Prettier (Code formatter)? [Y/n] ")),
726
+ };
727
+
728
+ let installPinoPretty = false;
729
+ if (deps["pino-http"]) {
730
+ installPinoPretty = parseYesNo(
731
+ await questioner.ask("Include pino-pretty for clean development logs? [Y/n] "),
732
+ );
733
+ }
734
+
735
+ const packageManagerChoice = await questioner.ask(
736
+ "\n> Which package manager would you like to use? [npm/yarn/pnpm/bun] (default: npm): ",
737
+ );
738
+ const packageManager = normalizePackageManager(packageManagerChoice);
739
+
740
+ const initGit = parseYesNo(await questioner.ask("\n> Initialize a git repository? [Y/n] "));
741
+ const initDocker = parseYesNo(
742
+ await questioner.ask("> Include Dockerfile & docker-compose.yml? [Y/n] "),
743
+ );
744
+ const initAuth = parseYesNo(
745
+ await questioner.ask("> Include basic JWT Auth boilerplate? [Y/n] "),
746
+ );
747
+ const initTests = parseYesNo(
748
+ await questioner.ask("> Include Jest setup and boilerplate tests? [Y/n] "),
749
+ );
750
+
751
+ const result = createProject(
752
+ {
753
+ projectName,
754
+ packageJsonName,
755
+ description,
756
+ author,
757
+ deps,
758
+ installPinoPretty,
759
+ packageManager,
760
+ initGit,
761
+ initDocker,
762
+ initAuth,
763
+ initTests,
764
+ },
765
+ { cwd, logger },
766
+ );
767
+
768
+ logger.log(`\n Success! Created "${projectName}" at ${result.projectPath}`);
769
+
770
+ if (result.warnings.length > 0) {
771
+ logger.log("\nNotes:");
772
+ for (const warning of result.warnings) {
773
+ logger.log(`- ${warning}`);
774
+ }
775
+ }
776
+
777
+ const devCommand =
778
+ result.config.packageManager === "npm"
779
+ ? "npm run dev"
780
+ : `${result.config.packageManager} dev`;
781
+ const startCommand =
782
+ result.config.packageManager === "npm"
783
+ ? "npm start"
784
+ : `${result.config.packageManager} start`;
785
+
786
+ logger.log("\nInside that directory, you can run:");
787
+ logger.log(`\n ${devCommand}`);
788
+ logger.log(" Starts the development server.");
789
+ logger.log(`\n ${startCommand}`);
790
+ logger.log(" Starts the production server.");
791
+ logger.log("\nWe suggest that you begin with:");
792
+ logger.log(`\n cd ${projectName}`);
793
+ logger.log(` ${devCommand}\n`);
794
+ } finally {
795
+ questioner.close();
796
+ }
797
+ };
798
+
799
+ if (process.argv[1] && path.resolve(process.argv[1]) === __filename) {
800
+ runCli().catch((error) => {
801
+ console.error(`\n${error.message}`);
802
+ process.exit(1);
803
+ });
804
+ }