nexforge-cli 1.0.5 → 1.0.6
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 +56 -10
- package/index.js +8 -523
- package/package.json +7 -2
- package/src/commands/create.js +261 -0
- package/src/commands/deploy.js +124 -0
- package/src/commands/env.js +102 -0
- package/src/commands/init.js +97 -0
- package/src/commands/login.js +50 -0
- package/src/commands/logs.js +74 -0
- package/src/commands/rename.js +103 -0
- package/src/commands/rollback.js +107 -0
- package/src/config.js +27 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The official Command Line Interface for **NexForge** - Your custom Platform as a Service (PaaS).
|
|
4
4
|
Deploy, manage, and scale your applications directly from your terminal with zero friction!
|
|
5
|
-
|
|
5
|
+
<a href="https://nexforge-zeta.vercel.app/">Visit Nexforge</a>
|
|
6
6
|
|
|
7
7
|
## Installation
|
|
8
8
|
|
|
@@ -17,12 +17,16 @@ npm install -g nexforge-cli
|
|
|
17
17
|
Here is the complete list of commands available in the NexForge CLI:
|
|
18
18
|
|
|
19
19
|
### 🔐 1. `nexforge login`
|
|
20
|
+
|
|
20
21
|
**Description:** Authenticate your local machine with your NexForge account.
|
|
21
22
|
**Usage:**
|
|
23
|
+
|
|
22
24
|
```bash
|
|
23
25
|
nexforge login
|
|
24
26
|
```
|
|
25
|
-
|
|
27
|
+
|
|
28
|
+
**Details:**
|
|
29
|
+
|
|
26
30
|
- Prompts you for your personal **CLI Token** (which you can generate from your NexForge Dashboard Settings).
|
|
27
31
|
- Prompts you for your **Project ID**.
|
|
28
32
|
- Saves your credentials securely in `~/.nexforge/config.json`.
|
|
@@ -30,25 +34,51 @@ nexforge login
|
|
|
30
34
|
---
|
|
31
35
|
|
|
32
36
|
### ✨ 2. `nexforge init`
|
|
37
|
+
|
|
33
38
|
**Description:** Initialize a new NexForge project right from your terminal without opening the browser.
|
|
34
39
|
**Usage:**
|
|
40
|
+
|
|
35
41
|
```bash
|
|
36
42
|
nexforge init
|
|
37
43
|
```
|
|
38
|
-
|
|
44
|
+
|
|
45
|
+
**Details:**
|
|
46
|
+
|
|
39
47
|
- Asks for your **Project Name**.
|
|
40
48
|
- Asks for the **Framework** you are using (e.g., React, Vue, Express, Node.js).
|
|
41
49
|
- Automatically registers the project on NexForge servers and links your current directory to the new Project ID.
|
|
42
50
|
|
|
43
51
|
---
|
|
44
52
|
|
|
45
|
-
###
|
|
53
|
+
### 🏗️ 3. `nexforge create`
|
|
54
|
+
|
|
55
|
+
**Description:** Scaffold a new project (Next.js, MERN, React, or Express) with industry-standard folder structures and NexForge's rich dependencies.
|
|
56
|
+
**Usage:**
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
nexforge create
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Details:**
|
|
63
|
+
|
|
64
|
+
- Interactively asks for your **Project Name** and **Tech Stack** (Next.js, MERN, Frontend, Backend).
|
|
65
|
+
- Generates a fully configured folder structure matching NexForge's custom standards.
|
|
66
|
+
- Pre-installs all necessary libraries (Tailwind, Framer Motion, AWS SDK, Mongoose, AI SDKs, Socket.io, etc.).
|
|
67
|
+
- Automatically sets up boilerplate code like `server.js` or `App.jsx` and configures TailwindCSS.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
### 🚀 4. `nexforge deploy`
|
|
72
|
+
|
|
46
73
|
**Description:** Package and deploy your current directory to NexForge.
|
|
47
74
|
**Usage:**
|
|
75
|
+
|
|
48
76
|
```bash
|
|
49
77
|
nexforge deploy
|
|
50
78
|
```
|
|
79
|
+
|
|
51
80
|
**Details:**
|
|
81
|
+
|
|
52
82
|
- Instantly zips your source code (ignoring `node_modules`, `.git`, and `.env`).
|
|
53
83
|
- Uploads the code to NexForge backend servers.
|
|
54
84
|
- Automatically connects to the live **WebSocket log stream** so you can watch your build progress in real-time.
|
|
@@ -56,54 +86,71 @@ nexforge deploy
|
|
|
56
86
|
|
|
57
87
|
---
|
|
58
88
|
|
|
59
|
-
### 🌐
|
|
89
|
+
### 🌐 5. `nexforge env`
|
|
90
|
+
|
|
60
91
|
**Description:** Manage your environment variables securely.
|
|
61
92
|
|
|
62
93
|
#### Push Variables
|
|
94
|
+
|
|
63
95
|
```bash
|
|
64
96
|
nexforge env push
|
|
65
97
|
```
|
|
98
|
+
|
|
66
99
|
- Reads your local `.env` file and securely pushes all variables to your NexForge project.
|
|
67
100
|
|
|
68
101
|
#### Pull Variables
|
|
102
|
+
|
|
69
103
|
```bash
|
|
70
104
|
nexforge env pull
|
|
71
105
|
```
|
|
106
|
+
|
|
72
107
|
- Fetches all environment variables from your live NexForge project and saves them into a local `.env` file.
|
|
73
108
|
|
|
74
109
|
---
|
|
75
110
|
|
|
76
|
-
### 📜
|
|
111
|
+
### 📜 6. `nexforge logs`
|
|
112
|
+
|
|
77
113
|
**Description:** Stream live build and deployment logs for your project.
|
|
78
114
|
**Usage:**
|
|
115
|
+
|
|
79
116
|
```bash
|
|
80
117
|
nexforge logs
|
|
81
118
|
```
|
|
82
|
-
|
|
119
|
+
|
|
120
|
+
**Details:**
|
|
121
|
+
|
|
83
122
|
- Connects to the NexForge WebSocket server and streams real-time logs for any ongoing deployment pipelines.
|
|
84
123
|
|
|
85
124
|
---
|
|
86
125
|
|
|
87
|
-
### ⏪
|
|
126
|
+
### ⏪ 7. `nexforge rollback`
|
|
127
|
+
|
|
88
128
|
**Description:** Instantly revert your live website to a previous stable deployment.
|
|
89
129
|
**Usage:**
|
|
130
|
+
|
|
90
131
|
```bash
|
|
91
132
|
nexforge rollback
|
|
92
133
|
```
|
|
134
|
+
|
|
93
135
|
**Details:**
|
|
136
|
+
|
|
94
137
|
- Fetches a list of your recent successful and failed deployments.
|
|
95
138
|
- Prompts you to select a version.
|
|
96
139
|
- Instantly updates the symbolic links on the server to serve the older version with **zero downtime** and **no rebuild required**.
|
|
97
140
|
|
|
98
141
|
---
|
|
99
142
|
|
|
100
|
-
### ✏️
|
|
143
|
+
### ✏️ 8. `nexforge rename`
|
|
144
|
+
|
|
101
145
|
**Description:** Rename the subdomain of your project.
|
|
102
146
|
**Usage:**
|
|
147
|
+
|
|
103
148
|
```bash
|
|
104
149
|
nexforge rename
|
|
105
150
|
```
|
|
151
|
+
|
|
106
152
|
**Details:**
|
|
153
|
+
|
|
107
154
|
- Asks for your new preferred subdomain.
|
|
108
155
|
- Validates availability on the NexForge servers.
|
|
109
156
|
- Instantly updates your live URL to the new subdomain.
|
|
@@ -111,4 +158,3 @@ nexforge rename
|
|
|
111
158
|
---
|
|
112
159
|
|
|
113
160
|
### Built With ❤️ by Manav Sharma
|
|
114
|
-
|
package/index.js
CHANGED
|
@@ -1,535 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { program } = require("commander")
|
|
4
|
-
const axios = require("axios")
|
|
5
|
-
const FormData = require("form-data")
|
|
6
|
-
const archiver = require("archiver")
|
|
7
|
-
const fs = require("fs")
|
|
8
|
-
const path = require("path")
|
|
9
|
-
const chalk = require("chalk")
|
|
10
|
-
const ora = require("ora")
|
|
11
|
-
|
|
12
|
-
// store user config to our home directory
|
|
13
|
-
const os = require("os")
|
|
14
|
-
const CONFIG_DIR = path.join(os.homedir(), ".nexforge")
|
|
15
|
-
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json")
|
|
16
|
-
|
|
17
|
-
const API_BASE_URL = "https://nexforge-backend.up.railway.app/api" // Make sure this points to your deployed backend URL in production
|
|
18
|
-
|
|
19
|
-
// Intercept axios requests to attach the CLI Token if it exists
|
|
20
|
-
axios.interceptors.request.use((config) => {
|
|
21
|
-
if (fs.existsSync(CONFIG_FILE)) {
|
|
22
|
-
const userConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"))
|
|
23
|
-
if (userConfig.cliToken) {
|
|
24
|
-
config.headers.Authorization = `Bearer ${userConfig.cliToken}`
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return config
|
|
28
|
-
})
|
|
29
4
|
|
|
30
5
|
program
|
|
31
6
|
.name("nexforge")
|
|
32
7
|
.description("CLI to some next-level deployment magic for NexForge")
|
|
33
8
|
.version("1.0.0")
|
|
34
9
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
program
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
output: process.stdout
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
console.log(chalk.cyan("🚀 Welcome to NexForge CLI!"))
|
|
47
|
-
|
|
48
|
-
readline.question("Please enter your personal CLI Token (Generate this from Dashboard Settings): ", (cliToken) => {
|
|
49
|
-
if (!cliToken) {
|
|
50
|
-
console.log(chalk.red("CLI Token cannot be empty!"))
|
|
51
|
-
readline.close()
|
|
52
|
-
return
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
readline.question("Please enter your Project ID: ", (projectId) => {
|
|
56
|
-
if (!projectId) {
|
|
57
|
-
console.log(chalk.red("Project ID cannot be empty!"))
|
|
58
|
-
readline.close()
|
|
59
|
-
return
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (!fs.existsSync(CONFIG_DIR)) {
|
|
63
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Save both Token and Project ID
|
|
67
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ cliToken, projectId }))
|
|
68
|
-
|
|
69
|
-
console.log(chalk.green("✅ Successfully logged in! You can now run `nexforge deploy`"))
|
|
70
|
-
readline.close()
|
|
71
|
-
})
|
|
72
|
-
})
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
// INIT COMMAND
|
|
76
|
-
// This allows a user to create a brand new NexForge project without leaving the terminal
|
|
77
|
-
program
|
|
78
|
-
.command("init")
|
|
79
|
-
.description("Initialize a new NexForge project in the current directory")
|
|
80
|
-
.action(async () => {
|
|
81
|
-
const readline = require("readline").createInterface({
|
|
82
|
-
input: process.stdin,
|
|
83
|
-
output: process.stdout
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
console.log(chalk.cyan("✨ Let's set up a new NexForge project!"))
|
|
87
|
-
|
|
88
|
-
readline.question("What is your project name? (e.g. my-app): ", async (projectName) => {
|
|
89
|
-
if (!projectName) {
|
|
90
|
-
console.log(chalk.red("Project name cannot be empty!"))
|
|
91
|
-
readline.close()
|
|
92
|
-
return
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
readline.question("What framework are you using? (React, Vue, Express, Node, Next.js): ", async (framework) => {
|
|
96
|
-
if (!framework) {
|
|
97
|
-
console.log(chalk.red("Framework cannot be empty!"))
|
|
98
|
-
readline.close()
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
const spinner = ora("Creating project on NexForge servers...").start()
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
const response = await axios.post(`${API_BASE_URL}/cli/init`, {
|
|
106
|
-
projectName,
|
|
107
|
-
framework,
|
|
108
|
-
projectType: ["express", "node"].includes(framework.toLowerCase()) ? "NODE" : "STATIC"
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
const { projectId } = response.data
|
|
112
|
-
|
|
113
|
-
// Ensure the config directory exists before saving
|
|
114
|
-
if (!fs.existsSync(CONFIG_DIR)) {
|
|
115
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// We preserve the existing CLI Token if it exists, and just update the projectId
|
|
119
|
-
let currentConfig = {}
|
|
120
|
-
if (fs.existsSync(CONFIG_FILE)) {
|
|
121
|
-
currentConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"))
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ ...currentConfig, projectId }))
|
|
125
|
-
|
|
126
|
-
spinner.succeed(chalk.green("🎉 Project created successfully!"))
|
|
127
|
-
console.log(chalk.cyan(`Project ID: ${projectId} has been saved locally.`))
|
|
128
|
-
console.log(chalk.yellow(`You can now run \`nexforge deploy\` to push your code.`))
|
|
129
|
-
|
|
130
|
-
} catch (error) {
|
|
131
|
-
spinner.fail(chalk.red("Failed to create project!"))
|
|
132
|
-
if (error.response && error.response.data) {
|
|
133
|
-
console.log(chalk.red(`Server Error: ${JSON.stringify(error.response.data)}`))
|
|
134
|
-
} else {
|
|
135
|
-
console.error(error.message)
|
|
136
|
-
}
|
|
137
|
-
} finally {
|
|
138
|
-
readline.close()
|
|
139
|
-
}
|
|
140
|
-
})
|
|
141
|
-
})
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
// DEPLOY COMMAND
|
|
146
|
-
// This is where the magic happens. We package the current folder and fire it up to our backend servers.
|
|
147
|
-
|
|
148
|
-
program
|
|
149
|
-
.command("deploy")
|
|
150
|
-
.description("Deploy the current directory to NexForge")
|
|
151
|
-
.action(async () => {
|
|
152
|
-
// First we check if they are logged in by reading the config file
|
|
153
|
-
if (!fs.existsSync(CONFIG_FILE)) {
|
|
154
|
-
console.log(chalk.red("❌ You are not logged in. Please run `nexforge login` first."))
|
|
155
|
-
return
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"))
|
|
159
|
-
const projectId = config.projectId
|
|
160
|
-
|
|
161
|
-
const spinner = ora("Packaging your project...").start()
|
|
162
|
-
const zipPath = path.join(process.cwd(), "nexforge-build.zip")
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
// We create a write stream to zip the contents of the current folder
|
|
166
|
-
const output = fs.createWriteStream(zipPath)
|
|
167
|
-
const archive = archiver("zip", { zlib: { level: 9 } }) // Maximum compression
|
|
168
|
-
|
|
169
|
-
// We need to wait for the archive to finish writing
|
|
170
|
-
const archivePromise = new Promise((resolve, reject) => {
|
|
171
|
-
output.on("close", resolve)
|
|
172
|
-
archive.on("error", reject)
|
|
173
|
-
})
|
|
174
|
-
|
|
175
|
-
archive.pipe(output)
|
|
176
|
-
|
|
177
|
-
// We grab everything in the current directory EXCEPT node_modules and hidden files
|
|
178
|
-
// Skipping node_modules is super important otherwise the upload would take forever
|
|
179
|
-
archive.glob("**/*", {
|
|
180
|
-
cwd: process.cwd(),
|
|
181
|
-
ignore: ["node_modules/**", ".git/**", "nexforge-build.zip", ".env"]
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
await archive.finalize()
|
|
185
|
-
await archivePromise
|
|
186
|
-
|
|
187
|
-
spinner.text = "Uploading to NexForge servers..."
|
|
188
|
-
|
|
189
|
-
// Now we prepare the multipart form-data request to send the zip file
|
|
190
|
-
const formData = new FormData()
|
|
191
|
-
formData.append("projectZip", fs.createReadStream(zipPath))
|
|
192
|
-
|
|
193
|
-
const response = await axios.post(`${API_BASE_URL}/cli/deploy/${projectId}`, formData, {
|
|
194
|
-
headers: {
|
|
195
|
-
...formData.getHeaders()
|
|
196
|
-
}
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
spinner.succeed(chalk.green("🎉 Deployment queued successfully!"))
|
|
200
|
-
console.log(chalk.cyan(`Live URL: ${response.data.liveUrl}`))
|
|
201
|
-
console.log(chalk.gray(`Streaming live build logs...\n`))
|
|
202
|
-
|
|
203
|
-
// Automatically stream logs after deployment
|
|
204
|
-
const io = require("socket.io-client")
|
|
205
|
-
const socket = io(API_BASE_URL.replace("/api", ""), {
|
|
206
|
-
transports: ["websocket", "polling"]
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
socket.emit("joinProject", projectId)
|
|
210
|
-
|
|
211
|
-
socket.on("new-log", (entry) => {
|
|
212
|
-
const color = entry.level === "ERROR" ? chalk.red : entry.level === "WARN" ? chalk.yellow : chalk.white
|
|
213
|
-
console.log(color(`[${new Date(entry.timestamp).toLocaleTimeString()}] ${entry.message}`))
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
socket.on("status-change", (data) => {
|
|
217
|
-
if (data.status === "LIVE") {
|
|
218
|
-
console.log(chalk.green(`\n✅ Deployment successful and Live!`))
|
|
219
|
-
socket.disconnect()
|
|
220
|
-
process.exit(0)
|
|
221
|
-
} else if (data.status === "FAILED") {
|
|
222
|
-
console.log(chalk.red(`\n❌ Deployment failed!`))
|
|
223
|
-
socket.disconnect()
|
|
224
|
-
process.exit(1)
|
|
225
|
-
}
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
} catch (error) {
|
|
229
|
-
spinner.fail(chalk.red("Deployment failed!"))
|
|
230
|
-
|
|
231
|
-
// If our API returned a specific error message we want to show that to the user
|
|
232
|
-
if (error.response && error.response.data) {
|
|
233
|
-
console.log(chalk.red(`Server Error: ${JSON.stringify(error.response.data)}`))
|
|
234
|
-
} else {
|
|
235
|
-
console.error(error.message)
|
|
236
|
-
}
|
|
237
|
-
} finally {
|
|
238
|
-
// Clean up the temporary zip file so we don't leave trash on the user's PC
|
|
239
|
-
if (fs.existsSync(zipPath)) {
|
|
240
|
-
fs.unlinkSync(zipPath)
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
// ROLLBACK COMMAND
|
|
246
|
-
|
|
247
|
-
program
|
|
248
|
-
.command("rollback")
|
|
249
|
-
.description("Rollback to the previous stable deployment")
|
|
250
|
-
.action(async () => {
|
|
251
|
-
console.log(chalk.yellow("🚧 Rollback command is under construction."))
|
|
252
|
-
console.log(chalk.cyan("Wait for the backend versioning architecture to be finalized before using this."))
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
// ENV COMMANDS
|
|
256
|
-
// Allows user to push and pull environment variables
|
|
257
|
-
const envCommand = program
|
|
258
|
-
.command("env")
|
|
259
|
-
.description("Manage your environment variables")
|
|
260
|
-
|
|
261
|
-
envCommand
|
|
262
|
-
.command("push")
|
|
263
|
-
.description("Push local .env file to NexForge")
|
|
264
|
-
.action(async () => {
|
|
265
|
-
if (!fs.existsSync(CONFIG_FILE)) {
|
|
266
|
-
console.log(chalk.red("❌ You are not logged in. Please run `nexforge login` first."))
|
|
267
|
-
return
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const envPath = path.join(process.cwd(), ".env")
|
|
271
|
-
if (!fs.existsSync(envPath)) {
|
|
272
|
-
console.log(chalk.red("❌ No .env file found in the current directory."))
|
|
273
|
-
return
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"))
|
|
277
|
-
const projectId = config.projectId
|
|
278
|
-
|
|
279
|
-
const spinner = ora("Pushing environment variables...").start()
|
|
280
|
-
try {
|
|
281
|
-
const dotenv = require("dotenv")
|
|
282
|
-
const envConfig = dotenv.parse(fs.readFileSync(envPath))
|
|
283
|
-
|
|
284
|
-
const envs = Object.keys(envConfig).map(key => ({
|
|
285
|
-
key,
|
|
286
|
-
value: envConfig[key]
|
|
287
|
-
}))
|
|
288
|
-
|
|
289
|
-
await axios.post(`${API_BASE_URL}/cli/env/push/${projectId}`, { envs })
|
|
290
|
-
|
|
291
|
-
spinner.succeed(chalk.green("🎉 Environment variables pushed successfully!"))
|
|
292
|
-
} catch (error) {
|
|
293
|
-
spinner.fail(chalk.red("Failed to push environment variables!"))
|
|
294
|
-
console.error(error.message)
|
|
295
|
-
}
|
|
296
|
-
})
|
|
297
|
-
|
|
298
|
-
envCommand
|
|
299
|
-
.command("pull")
|
|
300
|
-
.description("Pull environment variables from NexForge to local .env")
|
|
301
|
-
.action(async () => {
|
|
302
|
-
if (!fs.existsSync(CONFIG_FILE)) {
|
|
303
|
-
console.log(chalk.red("❌ You are not logged in. Please run `nexforge login` first."))
|
|
304
|
-
return
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"))
|
|
308
|
-
const projectId = config.projectId
|
|
309
|
-
|
|
310
|
-
const spinner = ora("Pulling environment variables...").start()
|
|
311
|
-
try {
|
|
312
|
-
const response = await axios.get(`${API_BASE_URL}/cli/env/pull/${projectId}`)
|
|
313
|
-
const envs = response.data.envs
|
|
314
|
-
|
|
315
|
-
if (!envs || envs.length === 0) {
|
|
316
|
-
spinner.info(chalk.yellow("No environment variables found on the server."))
|
|
317
|
-
return
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
let envContent = ""
|
|
321
|
-
envs.forEach(e => {
|
|
322
|
-
envContent += `${e.key}=${e.value}\n`
|
|
323
|
-
})
|
|
324
|
-
|
|
325
|
-
const envPath = path.join(process.cwd(), ".env")
|
|
326
|
-
fs.writeFileSync(envPath, envContent)
|
|
327
|
-
|
|
328
|
-
spinner.succeed(chalk.green("🎉 Environment variables pulled successfully!"))
|
|
329
|
-
console.log(chalk.cyan(`Saved to ${envPath}`))
|
|
330
|
-
} catch (error) {
|
|
331
|
-
spinner.fail(chalk.red("Failed to pull environment variables!"))
|
|
332
|
-
console.error(error.message)
|
|
333
|
-
}
|
|
334
|
-
})
|
|
335
|
-
|
|
336
|
-
// LOGS COMMAND
|
|
337
|
-
// Allows user to view live build logs for their project
|
|
338
|
-
program
|
|
339
|
-
.command("logs")
|
|
340
|
-
.description("Stream live build logs for your NexForge project")
|
|
341
|
-
.action(async () => {
|
|
342
|
-
if (!fs.existsSync(CONFIG_FILE)) {
|
|
343
|
-
console.log(chalk.red("❌ You are not logged in. Please run `nexforge login` first."))
|
|
344
|
-
return
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"))
|
|
348
|
-
const projectId = config.projectId
|
|
349
|
-
|
|
350
|
-
console.log(chalk.cyan(`🔌 Connecting to live log stream for project ${projectId}...`))
|
|
351
|
-
|
|
352
|
-
const io = require("socket.io-client")
|
|
353
|
-
const socket = io(API_BASE_URL.replace("/api", ""), {
|
|
354
|
-
transports: ["websocket", "polling"]
|
|
355
|
-
})
|
|
356
|
-
|
|
357
|
-
socket.emit("joinProject", projectId)
|
|
358
|
-
|
|
359
|
-
socket.on("initial-logs", (initialLogs) => {
|
|
360
|
-
initialLogs.forEach(entry => {
|
|
361
|
-
const color = entry.level === "ERROR" ? chalk.red : entry.level === "WARN" ? chalk.yellow : chalk.white
|
|
362
|
-
console.log(color(`[${new Date(entry.timestamp).toLocaleTimeString()}] ${entry.message}`))
|
|
363
|
-
})
|
|
364
|
-
})
|
|
365
|
-
|
|
366
|
-
socket.on("new-log", (entry) => {
|
|
367
|
-
const color = entry.level === "ERROR" ? chalk.red : entry.level === "WARN" ? chalk.yellow : chalk.white
|
|
368
|
-
console.log(color(`[${new Date(entry.timestamp).toLocaleTimeString()}] ${entry.message}`))
|
|
369
|
-
})
|
|
370
|
-
|
|
371
|
-
socket.on("status-change", (data) => {
|
|
372
|
-
if (data.status === "LIVE" || data.status === "FAILED") {
|
|
373
|
-
console.log(chalk.gray(`\nPipeline finished with status: ${data.status}`))
|
|
374
|
-
process.exit(0)
|
|
375
|
-
}
|
|
376
|
-
})
|
|
377
|
-
})
|
|
378
|
-
|
|
379
|
-
// ROLLBACK COMMAND
|
|
380
|
-
// Revert to a previous deployment version instantly
|
|
381
|
-
program
|
|
382
|
-
.command("rollback")
|
|
383
|
-
.description("Rollback your live website to a previous deployment")
|
|
384
|
-
.action(async () => {
|
|
385
|
-
if (!fs.existsSync(CONFIG_FILE)) {
|
|
386
|
-
console.log(chalk.red("❌ You are not logged in. Please run `nexforge login` first."))
|
|
387
|
-
return
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"))
|
|
391
|
-
const projectId = config.projectId
|
|
392
|
-
|
|
393
|
-
const spinner = ora("Fetching previous deployments...").start()
|
|
394
|
-
try {
|
|
395
|
-
const response = await axios.get(`${API_BASE_URL}/cli/deployments/${projectId}`)
|
|
396
|
-
const deployments = response.data.deployments
|
|
397
|
-
|
|
398
|
-
spinner.stop()
|
|
399
|
-
|
|
400
|
-
if (!deployments || deployments.length === 0) {
|
|
401
|
-
console.log(chalk.yellow("No previous deployments found."))
|
|
402
|
-
return
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
console.log(chalk.cyan("\nRecent Deployments:"))
|
|
406
|
-
deployments.forEach((dep, index) => {
|
|
407
|
-
const date = new Date(dep.createdAt).toLocaleString()
|
|
408
|
-
const statusColor = dep.status === "LIVE" ? chalk.green : dep.status === "FAILED" ? chalk.red : chalk.yellow
|
|
409
|
-
console.log(`[${index + 1}] ${dep._id} - ${statusColor(dep.status)} - ${date}`)
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
const readline = require("readline").createInterface({
|
|
413
|
-
input: process.stdin,
|
|
414
|
-
output: process.stdout
|
|
415
|
-
})
|
|
416
|
-
|
|
417
|
-
readline.question("\nEnter the number of the deployment you want to rollback to (or 'q' to cancel): ", async (answer) => {
|
|
418
|
-
if (answer.toLowerCase() === 'q') {
|
|
419
|
-
readline.close()
|
|
420
|
-
return
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
const index = parseInt(answer) - 1
|
|
424
|
-
if (isNaN(index) || index < 0 || index >= deployments.length) {
|
|
425
|
-
console.log(chalk.red("Invalid selection."))
|
|
426
|
-
readline.close()
|
|
427
|
-
return
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const selectedDeployment = deployments[index]
|
|
431
|
-
|
|
432
|
-
const rollbackSpinner = ora(`Rolling back to ${selectedDeployment._id}...`).start()
|
|
433
|
-
try {
|
|
434
|
-
await axios.post(`${API_BASE_URL}/cli/rollback/${projectId}`, {
|
|
435
|
-
deploymentId: selectedDeployment._id
|
|
436
|
-
})
|
|
437
|
-
rollbackSpinner.succeed(chalk.green(`🎉 Successfully rolled back to deployment ${selectedDeployment._id}!`))
|
|
438
|
-
console.log(chalk.cyan("Your website has been instantly updated. No rebuild required."))
|
|
439
|
-
} catch (error) {
|
|
440
|
-
rollbackSpinner.fail(chalk.red("Rollback failed!"))
|
|
441
|
-
if (error.response && error.response.data) {
|
|
442
|
-
console.log(chalk.red(`Error: ${error.response.data.error}`))
|
|
443
|
-
} else {
|
|
444
|
-
console.error(error.message)
|
|
445
|
-
}
|
|
446
|
-
} finally {
|
|
447
|
-
readline.close()
|
|
448
|
-
}
|
|
449
|
-
})
|
|
450
|
-
} catch (error) {
|
|
451
|
-
spinner.fail(chalk.red("Failed to fetch deployments!"))
|
|
452
|
-
console.error(error.message)
|
|
453
|
-
}
|
|
454
|
-
})
|
|
455
|
-
|
|
456
|
-
// change name of subdomain
|
|
457
|
-
program
|
|
458
|
-
.command("rename")
|
|
459
|
-
.description("Rename the subdomain of your project")
|
|
460
|
-
.action(async () => {
|
|
461
|
-
const readline = require("readline").createInterface({
|
|
462
|
-
input: process.stdin,
|
|
463
|
-
output: process.stdout
|
|
464
|
-
})
|
|
465
|
-
|
|
466
|
-
console.log(chalk.cyan("✨ Let's rename your project subdomain!"))
|
|
467
|
-
|
|
468
|
-
let defaultProjectId = ""
|
|
469
|
-
if (fs.existsSync(CONFIG_FILE)) {
|
|
470
|
-
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"))
|
|
471
|
-
defaultProjectId = config.projectId || ""
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const promptText = defaultProjectId
|
|
475
|
-
? `What is your project ID? (${defaultProjectId}) `
|
|
476
|
-
: "What is your project ID? "
|
|
477
|
-
|
|
478
|
-
readline.question(promptText, async (inputProjectId) => {
|
|
479
|
-
const projectId = inputProjectId.trim() || defaultProjectId
|
|
480
|
-
|
|
481
|
-
if (!projectId) {
|
|
482
|
-
console.log(chalk.red("Project ID cannot be empty!"))
|
|
483
|
-
readline.close()
|
|
484
|
-
return
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
readline.question("What is your new subdomain? ", async (newSubdomain) => {
|
|
488
|
-
if (!newSubdomain) {
|
|
489
|
-
console.log(chalk.red("Subdomain cannot be empty!"))
|
|
490
|
-
readline.close()
|
|
491
|
-
return
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
const spinner = ora("Renaming project subdomain...").start()
|
|
495
|
-
|
|
496
|
-
try {
|
|
497
|
-
const response = await axios.post(`${API_BASE_URL}/cli/rename/${projectId}`, {
|
|
498
|
-
newSubdomain
|
|
499
|
-
})
|
|
500
|
-
|
|
501
|
-
const { projectId } = response.data
|
|
502
|
-
|
|
503
|
-
// Ensure the config directory exists before saving
|
|
504
|
-
if (!fs.existsSync(CONFIG_DIR)) {
|
|
505
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// We preserve the existing CLI Token if it exists, and just update the projectId
|
|
509
|
-
let currentConfig = {}
|
|
510
|
-
if (fs.existsSync(CONFIG_FILE)) {
|
|
511
|
-
currentConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"))
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ ...currentConfig, projectId }))
|
|
515
|
-
|
|
516
|
-
spinner.succeed(chalk.green("🎉 Project subdomain renamed successfully!"))
|
|
517
|
-
console.log(chalk.cyan(`Project ID: ${projectId} has been saved locally.`))
|
|
518
|
-
console.log(chalk.yellow(`You can now run \`nexforge deploy\` to push your code.`))
|
|
519
|
-
|
|
520
|
-
} catch (error) {
|
|
521
|
-
spinner.fail(chalk.red("Failed to rename project subdomain!"))
|
|
522
|
-
if (error.response && error.response.data) {
|
|
523
|
-
console.log(chalk.red(`Server Error: ${JSON.stringify(error.response.data)}`))
|
|
524
|
-
} else {
|
|
525
|
-
console.error(error.message)
|
|
526
|
-
}
|
|
527
|
-
} finally {
|
|
528
|
-
readline.close()
|
|
529
|
-
}
|
|
530
|
-
})
|
|
531
|
-
})
|
|
532
|
-
})
|
|
10
|
+
require("./src/commands/login")(program)
|
|
11
|
+
require("./src/commands/init")(program)
|
|
12
|
+
require("./src/commands/deploy")(program)
|
|
13
|
+
require("./src/commands/env")(program)
|
|
14
|
+
require("./src/commands/logs")(program)
|
|
15
|
+
require("./src/commands/rollback")(program)
|
|
16
|
+
require("./src/commands/rename")(program)
|
|
17
|
+
require("./src/commands/create")(program)
|
|
533
18
|
|
|
534
19
|
// Parse the arguments passed by the user in the terminal
|
|
535
20
|
program.parse(process.argv)
|