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.
Files changed (3) hide show
  1. package/README.md +60 -0
  2. package/index.js +457 -0
  3. 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
+ }