nexforge-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/index.js +457 -0
- package/package.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# NexForge CLI 🚀
|
|
2
|
+
|
|
3
|
+
Welcome to the NexForge Command Line Interface. This tool allows you to deploy your local projects directly to the NexForge platform without pushing to GitHub.
|
|
4
|
+
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
If you are developing this CLI locally you can link it so it's available globally on your computer
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm link
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Now you can use the `nexforge` command from anywhere!
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
### 1. Login
|
|
18
|
+
Authenticate yourself with your NexForge Project ID.
|
|
19
|
+
```bash
|
|
20
|
+
nexforge login
|
|
21
|
+
```
|
|
22
|
+
This saves your configuration in your home directory `~/.nexforge/config.json`.
|
|
23
|
+
|
|
24
|
+
### 2. Deploy
|
|
25
|
+
Package your current directory and upload it to NexForge.
|
|
26
|
+
```bash
|
|
27
|
+
nexforge deploy
|
|
28
|
+
```
|
|
29
|
+
This command automatically zips your project (ignoring `node_modules` and `.env`) and sends it to the deployment queue.
|
|
30
|
+
|
|
31
|
+
### 3. Rollback
|
|
32
|
+
Revert your live site to the previous stable deployment.
|
|
33
|
+
```bash
|
|
34
|
+
nexforge rollback
|
|
35
|
+
```
|
|
36
|
+
*Note: This feature is currently in active development.*
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 📦 How to Publish to NPM
|
|
41
|
+
|
|
42
|
+
Once you are satisfied with this CLI and want to share it with the world you can publish it to the NPM registry. This allows anyone to install it by running `npm install -g nexforge-cli`.
|
|
43
|
+
|
|
44
|
+
### Step 1: Create an NPM Account
|
|
45
|
+
If you don't have one go to [npmjs.com](https://www.npmjs.com/) and create an account.
|
|
46
|
+
|
|
47
|
+
### Step 2: Login via Terminal
|
|
48
|
+
Run the following command in your terminal and enter your NPM credentials
|
|
49
|
+
```bash
|
|
50
|
+
npm login
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Step 3: Publish
|
|
54
|
+
Ensure your `package.json` has a unique name (e.g. `nexforge-cli`). Then simply run
|
|
55
|
+
```bash
|
|
56
|
+
npm publish
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
If everything is successful your package is now live on NPM!
|
|
60
|
+
Every time you make updates to the code you need to bump the version number in `package.json` (e.g. `"version": "1.0.1"`) before running `npm publish` again.
|
package/index.js
ADDED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
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
|
+
|
|
30
|
+
program
|
|
31
|
+
.name("nexforge")
|
|
32
|
+
.description("CLI to some next-level deployment magic for NexForge")
|
|
33
|
+
.version("1.0.0")
|
|
34
|
+
|
|
35
|
+
// LOGIN COMMAND
|
|
36
|
+
|
|
37
|
+
program
|
|
38
|
+
.command("login")
|
|
39
|
+
.description("Log in to your NexForge account")
|
|
40
|
+
.action(async () => {
|
|
41
|
+
const readline = require("readline").createInterface({
|
|
42
|
+
input: process.stdin,
|
|
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
|
+
// Parse the arguments passed by the user in the terminal
|
|
457
|
+
program.parse(process.argv)
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nexforge-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nexforge": "index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [],
|
|
13
|
+
"author": "",
|
|
14
|
+
"license": "ISC",
|
|
15
|
+
"type": "commonjs",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"archiver": "^5.3.1",
|
|
18
|
+
"axios": "^1.18.1",
|
|
19
|
+
"chalk": "^4.1.2",
|
|
20
|
+
"commander": "^15.0.0",
|
|
21
|
+
"dotenv": "^17.4.2",
|
|
22
|
+
"form-data": "^4.0.6",
|
|
23
|
+
"ignore": "^7.0.5",
|
|
24
|
+
"ora": "^5.4.1",
|
|
25
|
+
"socket.io-client": "^4.8.3"
|
|
26
|
+
}
|
|
27
|
+
}
|