neatnode 3.1.7 → 3.3.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 CHANGED
@@ -1,131 +1,135 @@
1
- <div align="center">
2
- <img src="https://i.postimg.cc/9Fvw7p3q/1762540009103.png" width="120" alt="NeatNode Logo" />
3
- <h1>NeatNode</h1>
4
- <p><strong>Instantly scaffold production-ready Node.js backends with one command.</strong></p>
5
-
6
- <img src="https://i.postimg.cc/59sw6LN6/1762540161048.png" alt="NeatNode Banner" />
7
- </div>
8
-
9
- ---
10
-
11
- ## Overview
12
-
13
- **NeatNode** is a plug-and-play CLI that scaffolds clean, production-ready **Node.js backends** in seconds.
14
- It comes with pre-built templates, optional CRUD modules, and a modern developer workflow — so you can start coding instead of configuring.
15
-
16
- ---
17
-
18
- ## Features
19
-
20
- - Multiple templates: **Basic API**, **REST API**, and **Socket API**
21
- - Clean MVC folder structure
22
- - Optional CRUD scaffolding (User or Todo)
23
- - Integrated logging (Winston + Morgan)
24
- - Security middleware (Helmet, Rate Limiter)
25
- - Dynamic CLI with file removal & template customization
26
- - Ready for ESM, Redis, and future TypeScript support
27
-
28
- ---
29
-
30
- ## Installation
31
-
32
- ```bash
33
- npm install -g neatnode
34
- ````
35
-
36
- or run directly with:
37
-
38
- ```bash
39
- npx neatnode
40
- ```
41
-
42
- ---
43
-
44
- ## Usage
45
-
46
- ```bash
47
- npx neatnode
48
- ```
49
-
50
- * Enter your **project name**
51
- * Choose a **template** (Basic / REST / Socket)
52
- * Select whether to include **CRUD** examples
53
- * Install dependencies and start your project:
54
-
55
- ```bash
56
- cd my-app
57
- npm install
58
- npm run dev
59
- ```
60
-
61
- ---
62
-
63
- ## Templates
64
-
65
- ### Basic API
66
-
67
- Minimal Express setup with optional Todo CRUD.
68
- Perfect for small projects or quick prototypes.
69
-
70
- ### REST API
71
-
72
- Full architecture with controllers, services, models, routes, error handling, validation, and logging.
73
- Ideal for scalable, production-grade APIs.
74
-
75
- ### Socket API
76
-
77
- Express + Socket.io integration with logging and CORS setup.
78
- Best for chat apps or real-time systems.
79
-
80
- ---
81
-
82
- ## Example Project Structure
83
-
84
- ```
85
- src/
86
- ├── controllers/
87
- ├── models/
88
- ├── routes/
89
- ├── services/
90
- ├── utils/
91
- └── app.js
92
- ```
93
-
94
- ---
95
-
96
- ## Built With
97
-
98
- * Node.js & Express
99
- * Inquirer (CLI interaction)
100
- * fs-extra & path (file system automation)
101
- * Morgan & Winston (logging)
102
- * Joi (validation)
103
- * Helmet, Rate Limiter (security)
104
-
105
- ---
106
-
107
- ## Project Screenshots
108
-
109
- ### Frontend Website
110
-
111
- > Official landing page for NeatNode <img src="https://i.postimg.cc/85QVd7pn/Untitled-design.png" alt="NeatNode Landing Page" />
112
-
113
- ---
114
-
115
- ### Documentation Website
116
-
117
- > Comprehensive docs built with Nextra <img src="https://i.postimg.cc/QNY0vR5r/Untitled-design-1.png" alt="NeatNode Docs" />
118
-
119
- ---
120
-
121
- ## Author
122
-
123
- **Aakash Gupta**
124
- · [GitHub](https://github.com/aakash-gupta02) · [Email](mailto:aakashgupta052004@gmail.com) ·
125
- [Website](https://neatnode.vercel.app) · [Docs](https://neatnodee-docs.vercel.app)
126
-
127
- ---
128
-
129
- ## License
130
-
131
- MIT © Aakash Gupta
1
+ <div align="center">
2
+ <img src="https://i.postimg.cc/9Fvw7p3q/1762540009103.png" width="120" alt="NeatNode Logo" />
3
+ <h1>NeatNode</h1>
4
+ <p><strong>Instantly scaffold production-ready Node.js backends with one command.</strong></p>
5
+
6
+ <img src="https://i.postimg.cc/59sw6LN6/1762540161048.png" alt="NeatNode Banner" />
7
+ </div>
8
+
9
+ ## Overview
10
+
11
+ **NeatNode** is a plug-and-play **CLI tool** that scaffolds clean, production-ready **Node.js backend projects** (JavaScript & TypeScript) in seconds.
12
+ It comes with pre-built templates, MVC or Modular architecture choices, optional CRUD modules, and a modern developer workflow - so you can **start coding instead of configuring.**
13
+
14
+ ## Features
15
+
16
+ - **Instant project setup** - generate a complete Node.js project structure instantly
17
+ - **JavaScript & TypeScript support** - choose your preferred language
18
+ - **Pre-configured templates** - Basic Express, REST API, and Socket.IO
19
+ - **Flexible Architecture** - pick between classic MVC or Modular architectures for REST APIs
20
+ - **Dynamic template logic** - easily include or exclude example CRUD modules (User, Todo, Auth)
21
+ - **Scalable architecture** - prebuilt folder layouts
22
+ - **Security-first setup** - Helmet, Rate Limiter, and CORS integrated
23
+ - **Built-in error handling & logging** - Winston + Morgan setup (depending on template)
24
+ - **Production-ready** - clean, validated, and deployment-ready output
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ # Install globally
30
+ npm install -g neatnode
31
+ ```
32
+
33
+ or run it directly using `npx`:
34
+
35
+ ```bash
36
+ npx neatnode
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ 1. Run the CLI:
42
+
43
+ ```bash
44
+ npx neatnode
45
+ ```
46
+ 2. Enter project folder name (default: `my-app`).
47
+ 3. Select Language:
48
+ * **JavaScript**
49
+ * **TypeScript**
50
+ 4. Choose your template (options depend on language).
51
+ 5. (Optional) For REST API in JavaScript, choose your architecture (MVC or Modular).
52
+ 6. (Optional) Incorporate example CRUD modules, like Todo, User, or Auth.
53
+ 7. Navigate and start building:
54
+
55
+ ```bash
56
+ cd <project_name>
57
+ npm install
58
+ npm run dev
59
+ ```
60
+
61
+ ## Available Templates
62
+
63
+ ### **JavaScript**
64
+ | Template | Description |
65
+ | ------------ | -------------------------------------------------------------------------------- |
66
+ | **Basic Express** | Minimal structure with .env, nodemon, and clean folder setup (Optional Todo CRUD) |
67
+ | **REST API** | Express.js REST API with controllers, routes, and MongoDB config. Supports **MVC** or **Modular** architecture. (Optional User CRUD) |
68
+ | **Socket.IO**| Socket.io server setup for real-time apps |
69
+
70
+ ### **TypeScript**
71
+ | Template | Description |
72
+ | ------------ | -------------------------------------------------------------------------------- |
73
+ | **Basic Express** | Minimal TS structure for Node.js (Optional Todo CRUD) |
74
+ | **REST API** | Express.js REST API boilerplate with TS. (Modular architectural setup with Optional Auth CRUD) |
75
+
76
+ ## Example CLI Output
77
+
78
+ ```
79
+ $ npx neatnode
80
+ 🚀 Welcome to NeatNode CLI!
81
+
82
+ ? Enter project folder name: api-server
83
+ ? Select language: TypeScript
84
+ ? Choose a template: REST API (TS)
85
+ ? Include example Auth CRUD? Yes
86
+
87
+ Project "api-server" created successfully using "REST API (TS)".
88
+
89
+ Next steps:
90
+ cd api-server
91
+ npm install
92
+ npm run dev
93
+
94
+ 🎉 Happy Coding!
95
+ ```
96
+
97
+ ## Previews
98
+
99
+ ### **Frontend Website**
100
+
101
+ > Official landing page for NeatNode
102
+ >
103
+ > <img width="1426" height="781" alt="NeatNode Landing Page" src="https://github.com/user-attachments/assets/cf7e3ae3-8830-44a3-bce2-74064545b2e5" />
104
+
105
+ ### **Documentation Website**
106
+
107
+ > Comprehensive docs built with Nextra
108
+ >
109
+ > <img width="1429" height="781" alt="NeatNode Docs" src="https://github.com/user-attachments/assets/59b1708f-d81c-405c-9bf7-3e8ee40bed85" />
110
+
111
+ ## Build With
112
+
113
+ * Node.js
114
+ * Inquirer (CLI prompts)
115
+ * Degit (repository fetching)
116
+ * Chalk & Ora
117
+
118
+ ## Learn More
119
+
120
+ * 📄 [Documentation](https://docs.neatnode.codes)
121
+ * 💻 [Website](https://neatnode.codes)
122
+ * 🧰 [NPM Package](https://www.npmjs.com/package/neatnode)
123
+
124
+ ## Author
125
+
126
+ **Aakash Gupta**
127
+ [GitHub](https://github.com/aakash-gupta02) • [Portfolio](https://aakashgupta.app)
128
+
129
+ ## License
130
+
131
+ This project is licensed under the **MIT License** feel free to use, modify, and distribute.
132
+
133
+ <div align="center">
134
+ <sub>Built with ❤️ by Aakash Gupta | NeatNode © 2026</sub>
135
+ </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neatnode",
3
- "version": "3.1.7",
3
+ "version": "3.3.0",
4
4
  "description": "Plug & Play Node.js backend starter templates — build REST APIs, socket servers, and more in seconds.",
5
5
  "bin": {
6
6
  "neatnode": "bin/index.js"
@@ -38,10 +38,20 @@
38
38
  "homepage": "https://github.com/aakash-gupta02/NeatNode#readme",
39
39
  "license": "ISC",
40
40
  "type": "module",
41
+ "scripts": {
42
+ "lint": "eslint .",
43
+ "lint:fix": "eslint . --fix",
44
+ "test": "node --test"
45
+ },
41
46
  "dependencies": {
42
47
  "axios": "^1.13.2",
43
48
  "extract-zip": "^2.0.1",
44
49
  "fs-extra": "^11.3.2",
45
50
  "inquirer": "^12.10.0"
51
+ },
52
+ "devDependencies": {
53
+ "@eslint/js": "^9.0.0",
54
+ "eslint": "^9.0.0",
55
+ "globals": "^15.0.0"
46
56
  }
47
- }
57
+ }
@@ -0,0 +1,20 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ export async function addEnv({ targetPath }) {
5
+ try {
6
+ const envExamplePath = path.join(targetPath, ".env.example");
7
+ const envPath = path.join(targetPath, ".env");
8
+
9
+ if (
10
+ fs.existsSync(envExamplePath) &&
11
+ !fs.existsSync(envPath)
12
+ ) {
13
+ const envContent = fs.readFileSync(envExamplePath, "utf8");
14
+
15
+ fs.writeFileSync(envPath, envContent, "utf8");
16
+ }
17
+ } catch (error) {
18
+ console.error("Error adding .env file:", error);
19
+ }
20
+ }
@@ -1,57 +1,76 @@
1
- import fs from "fs";
2
- import path from "path";
3
- import os from "os";
4
- import { fileURLToPath } from "url";
5
- import { copyTemplate } from "../utils/copyTemplate.js";
6
- import { removeCrud, removeCrudModule, removeCrudReferences } from "./removeCRUD.js";
7
- import { downloadTemplate } from "../utils/downloadRepoTemplate.js";
8
-
9
- const __filename = fileURLToPath(import.meta.url);
10
- const __dirname = path.dirname(__filename);
11
-
12
- export async function createProject({ projectName, repoPath, includeCrud, crudName, langKey, isModular }) {
13
- try {
14
- const targetPath = projectName === "."
15
- ? process.cwd()
16
- : path.join(process.cwd(), projectName);
17
-
18
- if (fs.existsSync(targetPath) && projectName !== ".") {
19
- console.error(`❌ Folder "${projectName}" already exists.`);
20
- process.exit(1);
21
- }
22
-
23
- if (projectName !== ".") {
24
- console.log("Creating project folder...");
25
- fs.mkdirSync(targetPath);
26
- }
27
-
28
- console.log("Downloading template...");
29
- const localTemplatePath = await downloadTemplate(repoPath);
30
-
31
- await copyTemplate(localTemplatePath, targetPath, {
32
- "project-name": projectName === "." ? path.basename(process.cwd()) : projectName,
33
- "author": os.userInfo().username || "author",
34
- });
35
-
36
- if (!includeCrud && crudName) {
37
- console.log("🗑 Removing CRUD files...");
38
-
39
- if (isModular) {
40
- removeCrudModule(targetPath, crudName);
41
- removeCrudReferences(path.join(targetPath, "src", `routes/index.route.${langKey}`));
42
-
43
- }
44
-
45
- removeCrud(targetPath, crudName, langKey);
46
- removeCrudReferences(path.join(targetPath, "src", `app.${langKey}`));
47
- }
48
-
49
- console.log(`\n✅ Project "${projectName}" created successfully!\n`);
50
-
51
- } catch (err) {
52
- console.error("❌ Failed to create project:", err);
53
- process.exit(1);
54
- }
55
- }
56
-
57
-
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+ import { fileURLToPath } from "url";
5
+ import { copyTemplate } from "../utils/copyTemplate.js";
6
+ import { cleanupTemplateMarkers, removeCrud, removeCrudModule, removeCrudReferences } from "./removeCRUD.js";
7
+ import { downloadTemplate } from "../utils/downloadRepoTemplateByVersionTags.js";
8
+ import { addEnv } from "./addEnv.js";
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+
13
+ export async function createProject({ projectName, repoPath, includeCrud, crudName, langKey, isModular }) {
14
+ try {
15
+ const targetPath = projectName === "."
16
+ ? process.cwd()
17
+ : path.join(process.cwd(), projectName);
18
+
19
+ if (fs.existsSync(targetPath) && projectName !== ".") {
20
+ console.error(`❌ Folder "${projectName}" already exists.`);
21
+ process.exit(1);
22
+ }
23
+
24
+ if (projectName !== ".") {
25
+ console.log("Creating project folder...");
26
+ fs.mkdirSync(targetPath);
27
+ }
28
+
29
+ console.log("Downloading template...");
30
+ const localTemplatePath = await downloadTemplate(repoPath);
31
+
32
+ await copyTemplate(localTemplatePath, targetPath, {
33
+ "project-name": projectName === "." ? path.basename(process.cwd()) : projectName,
34
+ "author": os.userInfo().username || "author",
35
+ });
36
+
37
+ await addEnv({ targetPath });
38
+
39
+ if (!includeCrud && crudName) {
40
+ console.log("🗑 Removing CRUD files...");
41
+
42
+ if (isModular) {
43
+ removeCrudModule(targetPath, crudName);
44
+
45
+ removeCrudReferences(
46
+ path.join(targetPath, "src", `routes/index.route.${langKey}`)
47
+ );
48
+ }
49
+
50
+ removeCrud(targetPath, crudName, langKey);
51
+
52
+ removeCrudReferences(
53
+ path.join(targetPath, "src", `app.${langKey}`)
54
+ );
55
+ }
56
+
57
+ // ALWAYS CLEANUP MARKERS
58
+ cleanupTemplateMarkers(
59
+ path.join(targetPath, "src", `app.${langKey}`)
60
+ );
61
+
62
+ if (isModular) {
63
+ cleanupTemplateMarkers(
64
+ path.join(targetPath, "src", `routes/index.route.${langKey}`)
65
+ );
66
+ }
67
+
68
+ console.log(`\n✅ Project "${projectName}" created successfully!\n`);
69
+
70
+ } catch (err) {
71
+ console.error("❌ Failed to create project:", err);
72
+ process.exit(1);
73
+ }
74
+ }
75
+
76
+
@@ -1,62 +1,88 @@
1
- import fs from "fs";
2
- import path from "path";
3
-
4
-
5
- export function removeCrud(targetPath, name, langKey) {
6
- try {
7
- const crudPaths = [
8
- `src/models/${name}.model.${langKey}`,
9
- `src/controllers/${name}.controller.${langKey}`,
10
- `src/routes/${name}.route.${langKey}`,
11
- `src/services/${name}.service.${langKey}`,
12
- `src/validations/${name}.validation.${langKey}`,
13
- `src/middlewares/auth.middleware.${langKey}`,
14
- `src/schemas/${name}.schema.${langKey}`
15
- ];
16
-
17
- crudPaths.forEach(relPath => {
18
- const absPath = path.join(targetPath, relPath);
19
-
20
- if (fs.existsSync(absPath)) {
21
- fs.rmSync(absPath, { force: true });
22
- console.log(`✔ Removed: ${relPath}`);
23
- } // else {
24
- // console.log(`Skipped (not found): ${relPath}`);
25
- // }
26
- });
27
- } catch (err) {
28
- console.error("❌ Error while removing CRUD files:", err.message);
29
- }
30
- }
31
-
32
- export function removeCrudReferences(appJsPath) {
33
- let content = fs.readFileSync(appJsPath, "utf8");
34
-
35
- // Remove imports block
36
- content = content.replace(
37
- /\/\/ ROUTE_IMPORTS_START[\s\S]*?\/\/ ROUTE_IMPORTS_END/,
38
- ""
39
- );
40
-
41
- // Remove route usage block
42
- content = content.replace(
43
- /\/\/ ROUTE_USES_START[\s\S]*?\/\/ ROUTE_USES_END/,
44
- ""
45
- );
46
-
47
- fs.writeFileSync(appJsPath, content, "utf8");
48
- }
49
-
50
- export function removeCrudModule(targetPath, name) {
51
- try {
52
- const modulePath = path.join(targetPath, `src/modules/${name}`);
53
- if (fs.existsSync(modulePath)) {
54
- fs.rmSync(modulePath, { recursive: true, force: true });
55
- console.log(`✔ Removed module: ${name}`);
56
- } else {
57
- console.log(`Skipped module (not found): ${name}`);
58
- }
59
- } catch (err) {
60
- console.error("❌ Error while removing CRUD module:", err.message);
61
- }
62
- }
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ export function removeCrud(targetPath, name, langKey) {
5
+ try {
6
+ const crudPaths = [
7
+ `src/models/${name}.model.${langKey}`,
8
+ `src/controllers/${name}.controller.${langKey}`,
9
+ `src/routes/${name}.route.${langKey}`,
10
+ `src/services/${name}.service.${langKey}`,
11
+ `src/validations/${name}.validation.${langKey}`,
12
+ `src/middlewares/auth.middleware.${langKey}`,
13
+ `src/schemas/${name}.schema.${langKey}`,
14
+ ];
15
+
16
+ crudPaths.forEach((relPath) => {
17
+ const absPath = path.join(targetPath, relPath);
18
+
19
+ if (fs.existsSync(absPath)) {
20
+ fs.rmSync(absPath, { force: true });
21
+ console.log(`✔ Removed: ${relPath}`);
22
+ } // else {
23
+ // console.log(`Skipped (not found): ${relPath}`);
24
+ // }
25
+ });
26
+ } catch (err) {
27
+ console.error("❌ Error while removing CRUD files:", err.message);
28
+ }
29
+ };
30
+
31
+ export function cleanupTemplateMarkers(filePath) {
32
+ if (!fs.existsSync(filePath)) {
33
+ return false;
34
+ }
35
+
36
+ let content = fs.readFileSync(filePath, "utf8");
37
+
38
+ content = content
39
+ .replace(/^\s*\/\/ ROUTE_IMPORTS_START\s*$/gm, "")
40
+ .replace(/^\s*\/\/ ROUTE_IMPORTS_END\s*$/gm, "")
41
+ .replace(/^\s*\/\/ ROUTE_USES_START\s*$/gm, "")
42
+ .replace(/^\s*\/\/ ROUTE_USES_END\s*$/gm, "");
43
+
44
+ // Remove excessive blank lines
45
+ content = content.replace(/\n{3,}/g, "\n\n");
46
+
47
+ fs.writeFileSync(filePath, content, "utf8");
48
+
49
+ return true;
50
+ };
51
+
52
+ export function removeCrudReferences(appJsPath) {
53
+ if (!fs.existsSync(appJsPath)) {
54
+ return false;
55
+ }
56
+
57
+ let content = fs.readFileSync(appJsPath, "utf8");
58
+
59
+ // Remove imports block + surrounding blank lines
60
+ content = content.replace(
61
+ /\s*\/\/ ROUTE_IMPORTS_START[\s\S]*?\/\/ ROUTE_IMPORTS_END\s*/g,
62
+ "\n",
63
+ );
64
+
65
+ // Remove route usage block + surrounding blank lines
66
+ content = content.replace(
67
+ /\s*\/\/ ROUTE_USES_START[\s\S]*?\/\/ ROUTE_USES_END\s*/g,
68
+ "\n",
69
+ );
70
+
71
+ fs.writeFileSync(appJsPath, content, "utf8");
72
+
73
+ return true;
74
+ };
75
+
76
+ export function removeCrudModule(targetPath, name) {
77
+ try {
78
+ const modulePath = path.join(targetPath, `src/modules/${name}`);
79
+ if (fs.existsSync(modulePath)) {
80
+ fs.rmSync(modulePath, { recursive: true, force: true });
81
+ console.log(`✔ Removed module: ${name}`);
82
+ } else {
83
+ console.log(`Skipped module (not found): ${name}`);
84
+ }
85
+ } catch (err) {
86
+ console.error("❌ Error while removing CRUD module:", err.message);
87
+ }
88
+ };
package/src/cli.js CHANGED
@@ -1,128 +1,147 @@
1
- #!/usr/bin/env node
2
- import inquirer from "inquirer";
3
- import templates from "./config/templates.js";
4
- import { createProject } from "./actions/createProject.js";
5
-
6
- async function main() {
7
- console.log("\n🚀 Welcome to NeatNode CLI!\n");
8
-
9
- // STEP 1 — Project Name
10
- const { projectName } = await inquirer.prompt([
11
- {
12
- type: "input",
13
- name: "projectName",
14
- message: "Enter project folder name:",
15
- default: "my-app",
16
- validate: (v) => v.trim() !== "" || "Project name cannot be empty.",
17
- },
18
- ]);
19
-
20
- // STEP 2 — Choose Language
21
- const { language } = await inquirer.prompt([
22
- {
23
- type: "list",
24
- name: "language",
25
- message: "Select language:",
26
- choices: ["JavaScript", "TypeScript"],
27
- },
28
- ]);
29
-
30
- const langKey = language === "JavaScript" ? "js" : "ts";
31
- const templateList = templates[langKey];
32
-
33
- // STEP 3 — Choose Template
34
- const { template } = await inquirer.prompt([
35
- {
36
- type: "list",
37
- name: "template",
38
- message: "Choose a template:",
39
- choices: templateList.map((t) => t.name),
40
- },
41
- ]);
42
-
43
- const chosen = templateList.find((t) => t.name === template);
44
-
45
- // STEP 4 CRUD Optional (only for some templates)
46
- let includeCrud = false;
47
- let isModular = chosen.isModular || false;
48
- let crudName = "";
49
-
50
- if (chosen.name === "Basic Express") {
51
- const { includeCrud: answer } = await inquirer.prompt([
52
- {
53
- type: "confirm",
54
- name: "includeCrud",
55
- message: "Include example Todo CRUD?",
56
- default: true,
57
- },
58
- ]);
59
- includeCrud = answer;
60
- crudName = "todo";
61
- }
62
-
63
- if (chosen.name === "REST API") {
64
- const { includeCrud: answer } = await inquirer.prompt([
65
- {
66
- type: "confirm",
67
- name: "includeCrud",
68
- message: "Include example User CRUD?",
69
- default: true,
70
- },
71
- ]);
72
- includeCrud = answer;
73
- crudName = "user";
74
- }
75
-
76
- if (chosen.name === "Basic Express (TS)") {
77
- const { includeCrud: answer } = await inquirer.prompt([
78
- {
79
- type: "confirm",
80
- name: "includeCrud",
81
- message: "Include example Todo CRUD?",
82
- default: true,
83
- },
84
- ]);
85
- includeCrud = answer;
86
- crudName = "todo";
87
- }
88
- if (chosen.name === "REST API (TS)") {
89
- const { includeCrud: answer } = await inquirer.prompt([
90
- {
91
- type: "confirm",
92
- name: "includeCrud",
93
- message: "Include example Auth CRUD?",
94
- default: true,
95
- },
96
- ]);
97
- includeCrud = answer;
98
- crudName = "auth";
99
- isModular = chosen.isModular;
100
- }
101
-
102
- // STEP 5 — Create Project (Remote download logic inside)
103
- await createProject({
104
- projectName,
105
- repoPath: chosen.repoPath,
106
- includeCrud,
107
- crudName,
108
- langKey,
109
- isModular,
110
- });
111
-
112
-
113
- console.log(`\n✅ Project "${projectName}" created successfully using "${chosen.name}".\n`);
114
-
115
- console.log("Next steps:");
116
- console.log(` cd ${projectName}`);
117
- console.log(" npm install");
118
- console.log(" npm run dev\n");
119
-
120
- console.log("🎉 Happy Coding!\n");
121
-
122
-
123
-
124
- }
125
-
126
- main().catch((err) => {
127
- console.error("❌ Error:", err.message || err);
128
- });
1
+ #!/usr/bin/env node
2
+ import inquirer from "inquirer";
3
+ import templates from "./config/templates.js";
4
+ import { createProject } from "./actions/createProject.js";
5
+
6
+ async function main() {
7
+ console.log("\n🚀 Welcome to NeatNode CLI!\n");
8
+
9
+ // STEP 1 — Project Name
10
+ const { projectName } = await inquirer.prompt([
11
+ {
12
+ type: "input",
13
+ name: "projectName",
14
+ message: "Enter project folder name:",
15
+ default: "my-app",
16
+ validate: (v) => v.trim() !== "" || "Project name cannot be empty.",
17
+ },
18
+ ]);
19
+
20
+ // STEP 2 — Choose Language
21
+ const { language } = await inquirer.prompt([
22
+ {
23
+ type: "list",
24
+ name: "language",
25
+ message: "Select language:",
26
+ choices: ["JavaScript", "TypeScript"],
27
+ },
28
+ ]);
29
+
30
+ const langKey = language === "JavaScript" ? "js" : "ts";
31
+ const templateList = templates[langKey];
32
+
33
+ // STEP 3 — Choose Template
34
+ const { template } = await inquirer.prompt([
35
+ {
36
+ type: "list",
37
+ name: "template",
38
+ message: "Choose a template:",
39
+ choices: templateList.map((t) => t.name),
40
+ },
41
+ ]);
42
+
43
+ const chosen = templateList.find((t) => t.name === template);
44
+
45
+ let isModular = chosen.isModular || false;
46
+ let architecture = null;
47
+
48
+ if (chosen.architecture) {
49
+ const answer = await inquirer.prompt([
50
+ {
51
+ type: "list",
52
+ name: "architecture",
53
+ message: "Select architecture:",
54
+ choices: ["mvc", "modular"],
55
+ },
56
+ ]);
57
+
58
+ architecture = answer.architecture;
59
+ isModular = architecture === "modular";
60
+
61
+ chosen.repoPath = chosen.architecture[architecture];
62
+
63
+ }
64
+
65
+ // STEP 4 — CRUD Optional (only for some templates)
66
+ let includeCrud = false;
67
+ let crudName = "";
68
+
69
+ if (chosen.name === "Basic Express") {
70
+ const { includeCrud: answer } = await inquirer.prompt([
71
+ {
72
+ type: "confirm",
73
+ name: "includeCrud",
74
+ message: "Include example Todo CRUD?",
75
+ default: true,
76
+ },
77
+ ]);
78
+ includeCrud = answer;
79
+ crudName = "todo";
80
+ }
81
+
82
+ if (chosen.name === "REST API") {
83
+ const { includeCrud: answer } = await inquirer.prompt([
84
+ {
85
+ type: "confirm",
86
+ name: "includeCrud",
87
+ message: "Include example User CRUD?",
88
+ default: true,
89
+ },
90
+ ]);
91
+ includeCrud = answer;
92
+ crudName = "user";
93
+ }
94
+
95
+ if (chosen.name === "Basic Express (TS)") {
96
+ const { includeCrud: answer } = await inquirer.prompt([
97
+ {
98
+ type: "confirm",
99
+ name: "includeCrud",
100
+ message: "Include example Todo CRUD?",
101
+ default: true,
102
+ },
103
+ ]);
104
+ includeCrud = answer;
105
+ crudName = "todo";
106
+ }
107
+ if (chosen.name === "REST API (TS)") {
108
+ const { includeCrud: answer } = await inquirer.prompt([
109
+ {
110
+ type: "confirm",
111
+ name: "includeCrud",
112
+ message: "Include example Auth CRUD?",
113
+ default: true,
114
+ },
115
+ ]);
116
+ includeCrud = answer;
117
+ crudName = "auth";
118
+ isModular = chosen.isModular;
119
+ }
120
+
121
+ // STEP 5 — Create Project (Remote download logic inside)
122
+ await createProject({
123
+ projectName,
124
+ repoPath: chosen.repoPath,
125
+ includeCrud,
126
+ crudName,
127
+ langKey,
128
+ isModular,
129
+ });
130
+
131
+
132
+ console.log(`\n✅ Project "${projectName}" created successfully using "${chosen.name}".\n`);
133
+
134
+ console.log("Next steps:");
135
+ console.log(` cd ${projectName}`);
136
+ console.log(" npm install");
137
+ console.log(" npm run dev\n");
138
+
139
+ console.log("🎉 Happy Coding!\n");
140
+
141
+
142
+
143
+ }
144
+
145
+ main().catch((err) => {
146
+ console.error("❌ Error:", err.message || err);
147
+ });
@@ -1,13 +1,19 @@
1
- export default {
2
- js: [
3
- { name: "Basic Express", repoPath: "templates/js/express-basic" },
4
- { name: "REST API", repoPath: "templates/js/express-rest-api" },
5
- { name: "Socket.IO", repoPath: "templates/js/express-socket" },
6
- ],
7
-
8
- ts: [
9
- { name: "Basic Express (TS)", repoPath: "templates/ts/basic-express" },
10
- { name: "REST API (TS)", repoPath: "templates/ts/express-rest-api", isModular: true },
11
- // { name: "Socket.IO (TS)", repoPath: "templates/ts/express-socket" },
12
- ],
13
- };
1
+ export default {
2
+ js: [
3
+ { name: "Basic Express", repoPath: "templates/js/express-basic" },
4
+ {
5
+ name: "REST API",
6
+ architecture: {
7
+ mvc: "templates/js/express-rest-api",
8
+ modular: "templates/js/express-modular-rest-api"
9
+ }
10
+ },
11
+ { name: "Socket.IO", repoPath: "templates/js/express-socket" },
12
+ ],
13
+
14
+ ts: [
15
+ { name: "Basic Express (TS)", repoPath: "templates/ts/basic-express" },
16
+ { name: "REST API (TS)", repoPath: "templates/ts/express-rest-api", isModular: true },
17
+ // { name: "Socket.IO (TS)", repoPath: "templates/ts/express-socket" },
18
+ ],
19
+ };
@@ -1,40 +1,48 @@
1
- import fs from "fs";
2
- import path from "path";
3
-
4
- export async function copyTemplate(srcDir, destDir, replacements = {}, ) {
5
- const ignoreList = ["node_modules", ".git", ".env", "package-lock.json", ".npmignore", "logs"];
6
-
7
- if (!fs.existsSync(destDir)) {
8
- fs.mkdirSync(destDir, { recursive: true });
9
- }
10
-
11
- const items = fs.readdirSync(srcDir, { withFileTypes: true });
12
-
13
- for (const item of items) {
14
- if (ignoreList.includes(item.name)) continue;
15
-
16
- const srcPath = path.join(srcDir, item.name);
17
- const destPath = path.join(destDir, item.name);
18
-
19
- if (item.isDirectory()) {
20
- // recursively copy folders
21
- await copyTemplate(srcPath, destPath, replacements);
22
- } else {
23
- // for certain files, replace placeholders
24
- if (["package.json"].includes(item.name)) {
25
- let content = fs.readFileSync(srcPath, "utf-8");
26
-
27
- // replace all {{key}} with provided replacements
28
- for (const [key, value] of Object.entries(replacements)) {
29
- const regex = new RegExp(`project-name`, "g");
30
- content = content.replace(regex, value);
31
- }
32
-
33
- fs.writeFileSync(destPath, content, "utf-8");
34
- } else {
35
- // just copy normal files
36
- fs.copyFileSync(srcPath, destPath);
37
- }
38
- }
39
- }
40
- }
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ export async function copyTemplate(srcDir, destDir, replacements = {}) {
5
+ const ignoreList = [
6
+ "node_modules",
7
+ ".git",
8
+ ".env",
9
+ "package-lock.json",
10
+ ".npmignore",
11
+ "logs",
12
+ ];
13
+
14
+ if (!fs.existsSync(destDir)) {
15
+ fs.mkdirSync(destDir, { recursive: true });
16
+ }
17
+
18
+ const items = fs.readdirSync(srcDir, { withFileTypes: true });
19
+
20
+ for (const item of items) {
21
+ if (ignoreList.includes(item.name)) continue;
22
+
23
+ const srcPath = path.join(srcDir, item.name);
24
+ const destPath = path.join(destDir, item.name);
25
+
26
+ if (item.isDirectory()) {
27
+ // recursively copy folders
28
+ await copyTemplate(srcPath, destPath, replacements);
29
+ } else {
30
+ // for certain files, replace placeholders
31
+ if (["package.json"].includes(item.name)) {
32
+ let content = fs.readFileSync(srcPath, "utf-8");
33
+
34
+ // Replace all configured {{key}} placeholders with their values.
35
+ for (const [key, value] of Object.entries(replacements)) {
36
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
37
+ const regex = new RegExp(`\\{\\{\\s*${escapedKey}\\s*\\}\\}`, "g");
38
+ content = content.replace(regex, String(value));
39
+ }
40
+
41
+ fs.writeFileSync(destPath, content, "utf-8");
42
+ } else {
43
+ // just copy normal files
44
+ fs.copyFileSync(srcPath, destPath);
45
+ }
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,127 @@
1
+ import axios from "axios";
2
+ import extract from "extract-zip";
3
+ import fs from "fs";
4
+ import path from "path";
5
+ import os from "os";
6
+ import { fileURLToPath } from "url";
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
11
+ const owner = "aakash-gupta02";
12
+ const repo = "NeatNode";
13
+
14
+ const getPackageVersion = () => {
15
+ try {
16
+ const pkgPath = path.resolve(__dirname, "../../package.json");
17
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
18
+ return pkg.version;
19
+ } catch {
20
+ return null;
21
+ }
22
+ };
23
+
24
+ const getTemplateRef = () => {
25
+ if (process.env.NEATNODE_TEMPLATE_REF) {
26
+ return process.env.NEATNODE_TEMPLATE_REF;
27
+ }
28
+
29
+ const version = getPackageVersion();
30
+
31
+ if (!version) {
32
+ return "main";
33
+ }
34
+
35
+ return `v${version}`;
36
+ };
37
+
38
+ const getZipUrl = (ref, refType = "tag") => {
39
+ if (refType === "branch") {
40
+ return `https://codeload.github.com/${owner}/${repo}/zip/refs/heads/${ref}`;
41
+ }
42
+ return `https://codeload.github.com/${owner}/${repo}/zip/refs/tags/${ref}`;
43
+ };
44
+
45
+ const downloadFromRef = async ({ repoPath, ref, refType }) => {
46
+ const zipUrl = getZipUrl(ref, refType);
47
+
48
+ const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), "neatnode-"));
49
+ const tempZip = path.join(tmpBase, "repo.zip");
50
+ const tempExtractDir = path.join(tmpBase, "repo-extract");
51
+ const tempFinalDir = path.join(tmpBase, "template-final");
52
+
53
+ const response = await axios({
54
+ url: zipUrl,
55
+ responseType: "arraybuffer",
56
+ });
57
+
58
+ fs.writeFileSync(tempZip, response.data);
59
+ await extract(tempZip, { dir: tempExtractDir });
60
+
61
+ const extractedRootDir = fs
62
+ .readdirSync(tempExtractDir, { withFileTypes: true })
63
+ .find((entry) => entry.isDirectory());
64
+
65
+ if (!extractedRootDir) {
66
+ throw new Error(
67
+ `Could not locate extracted template root directory for ${refType} "${ref}".`,
68
+ );
69
+ }
70
+
71
+ const extractedRoot = path.join(tempExtractDir, extractedRootDir.name);
72
+ const srcTemplatePath = path.join(extractedRoot, repoPath);
73
+
74
+ if (!fs.existsSync(srcTemplatePath)) {
75
+ throw new Error(
76
+ `Template path "${repoPath}" not found in ${refType} "${ref}" archive.`,
77
+ );
78
+ }
79
+
80
+ fs.mkdirSync(tempFinalDir, { recursive: true });
81
+ fs.cpSync(srcTemplatePath, tempFinalDir, { recursive: true });
82
+ return tempFinalDir;
83
+ };
84
+
85
+ export async function downloadTemplate(repoPath) {
86
+ const packageRoot = path.resolve(__dirname, "../..");
87
+ const packagedTemplatePath = path.join(packageRoot, repoPath);
88
+
89
+ if (fs.existsSync(packagedTemplatePath)) {
90
+ return packagedTemplatePath;
91
+ }
92
+
93
+ const preferredRef = getTemplateRef();
94
+ const candidates = [
95
+ { ref: preferredRef, refType: "tag" },
96
+ { ref: "main", refType: "branch" },
97
+ ];
98
+
99
+ const uniqueCandidates = candidates.filter(
100
+ (candidate, index, arr) =>
101
+ arr.findIndex(
102
+ (item) =>
103
+ item.ref === candidate.ref && item.refType === candidate.refType,
104
+ ) === index,
105
+ );
106
+
107
+ const errors = [];
108
+
109
+ for (const candidate of uniqueCandidates) {
110
+ try {
111
+ const templatePath = await downloadFromRef({
112
+ repoPath,
113
+ ref: candidate.ref,
114
+ refType: candidate.refType,
115
+ });
116
+
117
+ return templatePath;
118
+ } catch (error) {
119
+ const message = error instanceof Error ? error.message : String(error);
120
+ errors.push(`${candidate.refType}:${candidate.ref} -> ${message}`);
121
+ }
122
+ }
123
+
124
+ throw new Error(
125
+ `Failed to download template from all sources. ${errors.join(" | ")}`,
126
+ );
127
+ }
@@ -1,45 +0,0 @@
1
- import axios from "axios";
2
- import extract from "extract-zip";
3
- import fs from "fs";
4
- import path from "path";
5
- import os from "os";
6
- import { fileURLToPath } from "url";
7
-
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = path.dirname(__filename);
10
-
11
- const owner = "aakash-gupta02";
12
- const repo = "NeatNode";
13
-
14
- const zipUrl = `https://codeload.github.com/${owner}/${repo}/zip/refs/heads/main`;
15
-
16
- export async function downloadTemplate(repoPath) {
17
- // SAFE TEMP DIRECTORY
18
- const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), "neatnode-"));
19
-
20
- const tempZip = path.join(tmpBase, "repo.zip");
21
- const tempExtractDir = path.join(tmpBase, "repo-extract");
22
- const tempFinalDir = path.join(tmpBase, "template-final");
23
-
24
- // download zip
25
- const response = await axios({
26
- url: zipUrl,
27
- responseType: "arraybuffer",
28
- });
29
-
30
- fs.writeFileSync(tempZip, response.data);
31
-
32
- // unzip
33
- await extract(tempZip, { dir: tempExtractDir });
34
-
35
- const extractedRoot = path.join(tempExtractDir, `${repo}-main`);
36
- const srcTemplatePath = path.join(extractedRoot, repoPath);
37
-
38
- // copy template
39
- fs.mkdirSync(tempFinalDir, { recursive: true });
40
- fs.cpSync(srcTemplatePath, tempFinalDir, { recursive: true });
41
-
42
- console.log("✔ Template downloaded & extracted");
43
-
44
- return tempFinalDir;
45
- }