nexforge-cli 1.0.5 → 1.0.7
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 +316 -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
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const fs = require("fs")
|
|
2
|
+
const chalk = require("chalk")
|
|
3
|
+
const { CONFIG_DIR, CONFIG_FILE } = require("../config")
|
|
4
|
+
|
|
5
|
+
module.exports = (program) => {
|
|
6
|
+
program
|
|
7
|
+
.command("login")
|
|
8
|
+
.description("Log in to your NexForge account")
|
|
9
|
+
.action(async () => {
|
|
10
|
+
const readline = require("readline").createInterface({
|
|
11
|
+
input: process.stdin,
|
|
12
|
+
output: process.stdout,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
console.log(chalk.cyan("🚀 Welcome to NexForge CLI!"))
|
|
16
|
+
|
|
17
|
+
readline.question(
|
|
18
|
+
"Please enter your personal CLI Token (Generate this from Dashboard Settings): ",
|
|
19
|
+
(cliToken) => {
|
|
20
|
+
if (!cliToken) {
|
|
21
|
+
console.log(chalk.red("CLI Token cannot be empty!"))
|
|
22
|
+
readline.close()
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
readline.question("Please enter your Project ID: ", (projectId) => {
|
|
27
|
+
if (!projectId) {
|
|
28
|
+
console.log(chalk.red("Project ID cannot be empty!"))
|
|
29
|
+
readline.close()
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
34
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Save both Token and Project ID
|
|
38
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify({ cliToken, projectId }))
|
|
39
|
+
|
|
40
|
+
console.log(
|
|
41
|
+
chalk.green(
|
|
42
|
+
"✅ Successfully logged in! You can now run `nexforge deploy`",
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
readline.close()
|
|
46
|
+
})
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
})
|
|
50
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const fs = require("fs")
|
|
2
|
+
const chalk = require("chalk")
|
|
3
|
+
const { API_BASE_URL, CONFIG_FILE } = require("../config")
|
|
4
|
+
|
|
5
|
+
module.exports = (program) => {
|
|
6
|
+
program
|
|
7
|
+
.command("logs")
|
|
8
|
+
.description("Stream live build logs for your NexForge project")
|
|
9
|
+
.action(async () => {
|
|
10
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
11
|
+
console.log(
|
|
12
|
+
chalk.red(
|
|
13
|
+
"❌ You are not logged in. Please run `nexforge login` first.",
|
|
14
|
+
),
|
|
15
|
+
)
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"))
|
|
20
|
+
const projectId = config.projectId
|
|
21
|
+
|
|
22
|
+
console.log(
|
|
23
|
+
chalk.cyan(
|
|
24
|
+
`🔌 Connecting to live log stream for project ${projectId}...`,
|
|
25
|
+
),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
const io = require("socket.io-client")
|
|
29
|
+
const socket = io(API_BASE_URL.replace("/api", ""), {
|
|
30
|
+
transports: ["websocket", "polling"],
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
socket.emit("joinProject", projectId)
|
|
34
|
+
|
|
35
|
+
socket.on("initial-logs", (initialLogs) => {
|
|
36
|
+
initialLogs.forEach((entry) => {
|
|
37
|
+
const color =
|
|
38
|
+
entry.level === "ERROR"
|
|
39
|
+
? chalk.red
|
|
40
|
+
: entry.level === "WARN"
|
|
41
|
+
? chalk.yellow
|
|
42
|
+
: chalk.white
|
|
43
|
+
console.log(
|
|
44
|
+
color(
|
|
45
|
+
`[${new Date(entry.timestamp).toLocaleTimeString()}] ${entry.message}`,
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
socket.on("new-log", (entry) => {
|
|
52
|
+
const color =
|
|
53
|
+
entry.level === "ERROR"
|
|
54
|
+
? chalk.red
|
|
55
|
+
: entry.level === "WARN"
|
|
56
|
+
? chalk.yellow
|
|
57
|
+
: chalk.white
|
|
58
|
+
console.log(
|
|
59
|
+
color(
|
|
60
|
+
`[${new Date(entry.timestamp).toLocaleTimeString()}] ${entry.message}`,
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
socket.on("status-change", (data) => {
|
|
66
|
+
if (data.status === "LIVE" || data.status === "FAILED") {
|
|
67
|
+
console.log(
|
|
68
|
+
chalk.gray(`\nPipeline finished with status: ${data.status}`),
|
|
69
|
+
)
|
|
70
|
+
process.exit(0)
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const fs = require("fs")
|
|
2
|
+
const chalk = require("chalk")
|
|
3
|
+
const ora = require("ora")
|
|
4
|
+
const { axios, API_BASE_URL, CONFIG_DIR, CONFIG_FILE } = require("../config")
|
|
5
|
+
|
|
6
|
+
module.exports = (program) => {
|
|
7
|
+
program
|
|
8
|
+
.command("rename")
|
|
9
|
+
.description("Rename the subdomain of your project")
|
|
10
|
+
.action(async () => {
|
|
11
|
+
const readline = require("readline").createInterface({
|
|
12
|
+
input: process.stdin,
|
|
13
|
+
output: process.stdout,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
console.log(chalk.cyan("✨ Let's rename your project subdomain!"))
|
|
17
|
+
|
|
18
|
+
let defaultProjectId = ""
|
|
19
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
20
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"))
|
|
21
|
+
defaultProjectId = config.projectId || ""
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const promptText = defaultProjectId
|
|
25
|
+
? `What is your project ID? (${defaultProjectId}) `
|
|
26
|
+
: "What is your project ID? "
|
|
27
|
+
|
|
28
|
+
readline.question(promptText, async (inputProjectId) => {
|
|
29
|
+
const projectId = inputProjectId.trim() || defaultProjectId
|
|
30
|
+
|
|
31
|
+
if (!projectId) {
|
|
32
|
+
console.log(chalk.red("Project ID cannot be empty!"))
|
|
33
|
+
readline.close()
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
readline.question(
|
|
38
|
+
"What is your new subdomain? ",
|
|
39
|
+
async (newSubdomain) => {
|
|
40
|
+
if (!newSubdomain) {
|
|
41
|
+
console.log(chalk.red("Subdomain cannot be empty!"))
|
|
42
|
+
readline.close()
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const spinner = ora("Renaming project subdomain...").start()
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const response = await axios.post(
|
|
50
|
+
`${API_BASE_URL}/cli/rename/${projectId}`,
|
|
51
|
+
{
|
|
52
|
+
newSubdomain,
|
|
53
|
+
},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
const { projectId: returnedProjectId } = response.data
|
|
57
|
+
|
|
58
|
+
// Ensure the config directory exists before saving
|
|
59
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
60
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// We preserve the existing CLI Token if it exists, and just update the projectId
|
|
64
|
+
let currentConfig = {}
|
|
65
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
66
|
+
currentConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fs.writeFileSync(
|
|
70
|
+
CONFIG_FILE,
|
|
71
|
+
JSON.stringify({ ...currentConfig, projectId: returnedProjectId }),
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
spinner.succeed(
|
|
75
|
+
chalk.green("🎉 Project subdomain renamed successfully!"),
|
|
76
|
+
)
|
|
77
|
+
console.log(
|
|
78
|
+
chalk.cyan(`Project ID: ${returnedProjectId} has been saved locally.`),
|
|
79
|
+
)
|
|
80
|
+
console.log(
|
|
81
|
+
chalk.yellow(
|
|
82
|
+
`You can now run \`nexforge deploy\` to push your code.`,
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
} catch (error) {
|
|
86
|
+
spinner.fail(chalk.red("Failed to rename project subdomain!"))
|
|
87
|
+
if (error.response && error.response.data) {
|
|
88
|
+
console.log(
|
|
89
|
+
chalk.red(
|
|
90
|
+
`Server Error: ${JSON.stringify(error.response.data)}`,
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
} else {
|
|
94
|
+
console.error(error.message)
|
|
95
|
+
}
|
|
96
|
+
} finally {
|
|
97
|
+
readline.close()
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const fs = require("fs")
|
|
2
|
+
const chalk = require("chalk")
|
|
3
|
+
const ora = require("ora")
|
|
4
|
+
const { axios, API_BASE_URL, CONFIG_FILE } = require("../config")
|
|
5
|
+
|
|
6
|
+
module.exports = (program) => {
|
|
7
|
+
program
|
|
8
|
+
.command("rollback")
|
|
9
|
+
.description("Rollback your live website to a previous deployment")
|
|
10
|
+
.action(async () => {
|
|
11
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
12
|
+
console.log(
|
|
13
|
+
chalk.red(
|
|
14
|
+
"❌ You are not logged in. Please run `nexforge login` first.",
|
|
15
|
+
),
|
|
16
|
+
)
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"))
|
|
21
|
+
const projectId = config.projectId
|
|
22
|
+
|
|
23
|
+
const spinner = ora("Fetching previous deployments...").start()
|
|
24
|
+
try {
|
|
25
|
+
const response = await axios.get(
|
|
26
|
+
`${API_BASE_URL}/cli/deployments/${projectId}`,
|
|
27
|
+
)
|
|
28
|
+
const deployments = response.data.deployments
|
|
29
|
+
|
|
30
|
+
spinner.stop()
|
|
31
|
+
|
|
32
|
+
if (!deployments || deployments.length === 0) {
|
|
33
|
+
console.log(chalk.yellow("No previous deployments found."))
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log(chalk.cyan("\nRecent Deployments:"))
|
|
38
|
+
deployments.forEach((dep, index) => {
|
|
39
|
+
const date = new Date(dep.createdAt).toLocaleString()
|
|
40
|
+
const statusColor =
|
|
41
|
+
dep.status === "LIVE"
|
|
42
|
+
? chalk.green
|
|
43
|
+
: dep.status === "FAILED"
|
|
44
|
+
? chalk.red
|
|
45
|
+
: chalk.yellow
|
|
46
|
+
console.log(
|
|
47
|
+
`[${index + 1}] ${dep._id} - ${statusColor(dep.status)} - ${date}`,
|
|
48
|
+
)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const readline = require("readline").createInterface({
|
|
52
|
+
input: process.stdin,
|
|
53
|
+
output: process.stdout,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
readline.question(
|
|
57
|
+
"\nEnter the number of the deployment you want to rollback to (or 'q' to cancel): ",
|
|
58
|
+
async (answer) => {
|
|
59
|
+
if (answer.toLowerCase() === "q") {
|
|
60
|
+
readline.close()
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const index = parseInt(answer) - 1
|
|
65
|
+
if (isNaN(index) || index < 0 || index >= deployments.length) {
|
|
66
|
+
console.log(chalk.red("Invalid selection."))
|
|
67
|
+
readline.close()
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const selectedDeployment = deployments[index]
|
|
72
|
+
|
|
73
|
+
const rollbackSpinner = ora(
|
|
74
|
+
`Rolling back to ${selectedDeployment._id}...`,
|
|
75
|
+
).start()
|
|
76
|
+
try {
|
|
77
|
+
await axios.post(`${API_BASE_URL}/cli/rollback/${projectId}`, {
|
|
78
|
+
deploymentId: selectedDeployment._id,
|
|
79
|
+
})
|
|
80
|
+
rollbackSpinner.succeed(
|
|
81
|
+
chalk.green(
|
|
82
|
+
`🎉 Successfully rolled back to deployment ${selectedDeployment._id}!`,
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
console.log(
|
|
86
|
+
chalk.cyan(
|
|
87
|
+
"Your website has been instantly updated. No rebuild required.",
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
} catch (error) {
|
|
91
|
+
rollbackSpinner.fail(chalk.red("Rollback failed!"))
|
|
92
|
+
if (error.response && error.response.data) {
|
|
93
|
+
console.log(chalk.red(`Error: ${error.response.data.error}`))
|
|
94
|
+
} else {
|
|
95
|
+
console.error(error.message)
|
|
96
|
+
}
|
|
97
|
+
} finally {
|
|
98
|
+
readline.close()
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
} catch (error) {
|
|
103
|
+
spinner.fail(chalk.red("Failed to fetch deployments!"))
|
|
104
|
+
console.error(error.message)
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const axios = require("axios")
|
|
2
|
+
const fs = require("fs")
|
|
3
|
+
const path = require("path")
|
|
4
|
+
const os = require("os")
|
|
5
|
+
|
|
6
|
+
const CONFIG_DIR = path.join(os.homedir(), ".nexforge")
|
|
7
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json")
|
|
8
|
+
|
|
9
|
+
const API_BASE_URL = "https://nexforge-lbxg.onrender.com/api"
|
|
10
|
+
|
|
11
|
+
// Intercept axios requests to attach the CLI Token if it exists
|
|
12
|
+
axios.interceptors.request.use((config) => {
|
|
13
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
14
|
+
const userConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"))
|
|
15
|
+
if (userConfig.cliToken) {
|
|
16
|
+
config.headers.Authorization = `Bearer ${userConfig.cliToken}`
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return config
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
CONFIG_DIR,
|
|
24
|
+
CONFIG_FILE,
|
|
25
|
+
API_BASE_URL,
|
|
26
|
+
axios
|
|
27
|
+
}
|