nexforge-cli 1.0.4 ā 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 -9
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nexforge-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Custom Deployment CLI for NexForge",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
11
|
},
|
|
12
|
-
"keywords": [
|
|
12
|
+
"keywords": [
|
|
13
|
+
"cli",
|
|
14
|
+
"deployment",
|
|
15
|
+
"nexforge"
|
|
16
|
+
],
|
|
13
17
|
"author": "Manav Sharma",
|
|
14
18
|
"license": "ISC",
|
|
15
19
|
"type": "commonjs",
|
|
@@ -21,6 +25,7 @@
|
|
|
21
25
|
"dotenv": "^17.4.2",
|
|
22
26
|
"form-data": "^4.0.6",
|
|
23
27
|
"ignore": "^7.0.5",
|
|
28
|
+
"inquirer": "^8.2.6",
|
|
24
29
|
"ora": "^5.4.1",
|
|
25
30
|
"socket.io-client": "^4.8.3"
|
|
26
31
|
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
const fs = require("fs")
|
|
2
|
+
const path = require("path")
|
|
3
|
+
const chalk = require("chalk")
|
|
4
|
+
const ora = require("ora")
|
|
5
|
+
const inquirer = require("inquirer")
|
|
6
|
+
const { execSync } = require("child_process")
|
|
7
|
+
|
|
8
|
+
const BACKEND_DEPS = {
|
|
9
|
+
"@aws-sdk/client-s3": "^3.1075.0",
|
|
10
|
+
"@aws-sdk/lib-storage": "^3.1075.0",
|
|
11
|
+
"@google/genai": "^2.10.0",
|
|
12
|
+
"axios": "^1.18.0",
|
|
13
|
+
"bullmq": "^5.79.1",
|
|
14
|
+
"cookie-parser": "^1.4.7",
|
|
15
|
+
"cors": "^2.8.6",
|
|
16
|
+
"dotenv": "^17.4.2",
|
|
17
|
+
"express": "^5.2.1",
|
|
18
|
+
"express-rate-limit": "^8.5.2",
|
|
19
|
+
"extract-zip": "^2.0.1",
|
|
20
|
+
"groq-sdk": "^1.3.0",
|
|
21
|
+
"http-proxy-middleware": "^4.1.1",
|
|
22
|
+
"ioredis": "^5.11.1",
|
|
23
|
+
"jsonwebtoken": "^9.0.3",
|
|
24
|
+
"mime-types": "^3.0.2",
|
|
25
|
+
"mongoose": "^9.7.1",
|
|
26
|
+
"multer": "^2.2.0",
|
|
27
|
+
"node-cron": "^4.5.0",
|
|
28
|
+
"openai": "^6.44.0",
|
|
29
|
+
"os-utils": "^0.0.14",
|
|
30
|
+
"pm2": "^7.0.1",
|
|
31
|
+
"portfinder": "^1.0.38",
|
|
32
|
+
"redis": "^6.0.0",
|
|
33
|
+
"socket.io": "^4.8.3"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const FRONTEND_DEPS = {
|
|
37
|
+
"@heroicons/react": "^2.2.0",
|
|
38
|
+
"axios": "^1.18.0",
|
|
39
|
+
"clsx": "^2.1.1",
|
|
40
|
+
"framer-motion": "^12.40.0",
|
|
41
|
+
"gsap": "^3.15.0",
|
|
42
|
+
"lenis": "^1.3.23",
|
|
43
|
+
"lucide-react": "^1.21.0",
|
|
44
|
+
"react-icons": "^5.6.0",
|
|
45
|
+
"react-toastify": "^11.1.0",
|
|
46
|
+
"recharts": "^3.8.1",
|
|
47
|
+
"socket.io-client": "^4.8.3",
|
|
48
|
+
"tailwind-merge": "^3.6.0"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const NEXTJS_DEPS = { ...FRONTEND_DEPS, ...BACKEND_DEPS }
|
|
52
|
+
|
|
53
|
+
const createFolderStructure = (baseDir, folders) => {
|
|
54
|
+
folders.forEach((folder) => {
|
|
55
|
+
fs.mkdirSync(path.join(baseDir, folder), { recursive: true })
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const runCommand = (command, cwd) => {
|
|
60
|
+
try {
|
|
61
|
+
execSync(command, { cwd, stdio: "inherit" })
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(chalk.red(`Failed to execute: ${command}`))
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const setupBackend = (targetDir) => {
|
|
68
|
+
console.log(chalk.cyan("\nš Scaffolding Backend (Express/Node)..."))
|
|
69
|
+
fs.mkdirSync(targetDir, { recursive: true })
|
|
70
|
+
createFolderStructure(path.join(targetDir, "src"), [
|
|
71
|
+
"controllers",
|
|
72
|
+
"models",
|
|
73
|
+
"routes",
|
|
74
|
+
"middlewares",
|
|
75
|
+
"services",
|
|
76
|
+
"utils",
|
|
77
|
+
"config",
|
|
78
|
+
])
|
|
79
|
+
|
|
80
|
+
const pkgJson = {
|
|
81
|
+
name: "backend",
|
|
82
|
+
version: "1.0.0",
|
|
83
|
+
main: "src/server.js",
|
|
84
|
+
scripts: {
|
|
85
|
+
dev: "nodemon src/server.js",
|
|
86
|
+
start: "node src/server.js",
|
|
87
|
+
},
|
|
88
|
+
dependencies: BACKEND_DEPS,
|
|
89
|
+
devDependencies: {
|
|
90
|
+
nodemon: "^3.1.14",
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fs.writeFileSync(path.join(targetDir, "package.json"), JSON.stringify(pkgJson, null, 2))
|
|
95
|
+
fs.writeFileSync(path.join(targetDir, ".env"), "PORT=8000\nMONGO_URI=")
|
|
96
|
+
fs.writeFileSync(path.join(targetDir, ".gitignore"), "node_modules\n.env\n")
|
|
97
|
+
|
|
98
|
+
const serverJs = `const express = require('express');
|
|
99
|
+
const cors = require('cors');
|
|
100
|
+
require('dotenv').config();
|
|
101
|
+
|
|
102
|
+
const app = express();
|
|
103
|
+
app.use(cors());
|
|
104
|
+
app.use(express.json());
|
|
105
|
+
|
|
106
|
+
app.get('/', (req, res) => res.send('NexForge Backend API is running!'));
|
|
107
|
+
|
|
108
|
+
const PORT = process.env.PORT || 8000;
|
|
109
|
+
app.listen(PORT, () => console.log(\`Server running on port \${PORT}\`));
|
|
110
|
+
`
|
|
111
|
+
fs.writeFileSync(path.join(targetDir, "src", "server.js"), serverJs)
|
|
112
|
+
|
|
113
|
+
console.log(chalk.yellow("š¦ Installing backend dependencies (this may take a while)..."))
|
|
114
|
+
runCommand("npm install", targetDir)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const setupFrontend = (targetDir) => {
|
|
118
|
+
console.log(chalk.cyan("\nāļø Scaffolding Frontend (React + Vite)..."))
|
|
119
|
+
runCommand(`npx create-vite@latest ${path.basename(targetDir)} --template react`, path.dirname(targetDir))
|
|
120
|
+
|
|
121
|
+
createFolderStructure(path.join(targetDir, "src"), [
|
|
122
|
+
"assets",
|
|
123
|
+
"components",
|
|
124
|
+
"hooks",
|
|
125
|
+
"lib",
|
|
126
|
+
"pages",
|
|
127
|
+
"redux",
|
|
128
|
+
"services",
|
|
129
|
+
"utils",
|
|
130
|
+
])
|
|
131
|
+
|
|
132
|
+
// Merge package.json
|
|
133
|
+
const pkgPath = path.join(targetDir, "package.json")
|
|
134
|
+
if (fs.existsSync(pkgPath)) {
|
|
135
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
|
|
136
|
+
pkg.dependencies = { ...pkg.dependencies, ...FRONTEND_DEPS, "react-router-dom": "^7.18.0" }
|
|
137
|
+
pkg.devDependencies = { ...pkg.devDependencies, tailwindcss: "^3.4.19", postcss: "^8.5.15", autoprefixer: "^10.5.0" }
|
|
138
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Tailwind config
|
|
142
|
+
const twConfig = `/** @type {import('tailwindcss').Config} */
|
|
143
|
+
export default {
|
|
144
|
+
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
|
145
|
+
theme: { extend: {} },
|
|
146
|
+
plugins: [],
|
|
147
|
+
}`
|
|
148
|
+
fs.writeFileSync(path.join(targetDir, "tailwind.config.js"), twConfig)
|
|
149
|
+
fs.writeFileSync(path.join(targetDir, "postcss.config.js"), `export default { plugins: { tailwindcss: {}, autoprefixer: {} } }`)
|
|
150
|
+
|
|
151
|
+
const css = `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n`
|
|
152
|
+
fs.writeFileSync(path.join(targetDir, "src", "index.css"), css)
|
|
153
|
+
|
|
154
|
+
// Utils
|
|
155
|
+
const utilsJs = `import { clsx } from "clsx"
|
|
156
|
+
import { twMerge } from "tailwind-merge"
|
|
157
|
+
|
|
158
|
+
export function cn(...inputs) {
|
|
159
|
+
return twMerge(clsx(inputs))
|
|
160
|
+
}
|
|
161
|
+
`
|
|
162
|
+
fs.writeFileSync(path.join(targetDir, "src", "lib", "utils.js"), utilsJs)
|
|
163
|
+
|
|
164
|
+
console.log(chalk.yellow("š¦ Installing frontend dependencies..."))
|
|
165
|
+
runCommand("npm install", targetDir)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const setupNextJs = (targetDir) => {
|
|
169
|
+
console.log(chalk.cyan("\nš Scaffolding Next.js App Router Project..."))
|
|
170
|
+
// We use npx create-next-app
|
|
171
|
+
const command = `npx create-next-app@latest ${path.basename(targetDir)} --js --tailwind --eslint --app --src-dir --import-alias "@/*"`
|
|
172
|
+
runCommand(command, path.dirname(targetDir))
|
|
173
|
+
|
|
174
|
+
// Create custom folders
|
|
175
|
+
createFolderStructure(path.join(targetDir, "src"), [
|
|
176
|
+
"app/api",
|
|
177
|
+
"components",
|
|
178
|
+
"hooks",
|
|
179
|
+
"lib",
|
|
180
|
+
"store",
|
|
181
|
+
"services",
|
|
182
|
+
"utils",
|
|
183
|
+
])
|
|
184
|
+
|
|
185
|
+
// Merge package.json
|
|
186
|
+
const pkgPath = path.join(targetDir, "package.json")
|
|
187
|
+
if (fs.existsSync(pkgPath)) {
|
|
188
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
|
|
189
|
+
pkg.dependencies = { ...pkg.dependencies, ...NEXTJS_DEPS }
|
|
190
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const utilsJs = `import { clsx } from "clsx"
|
|
194
|
+
import { twMerge } from "tailwind-merge"
|
|
195
|
+
|
|
196
|
+
export function cn(...inputs) {
|
|
197
|
+
return twMerge(clsx(inputs))
|
|
198
|
+
}
|
|
199
|
+
`
|
|
200
|
+
fs.writeFileSync(path.join(targetDir, "src", "lib", "utils.js"), utilsJs)
|
|
201
|
+
|
|
202
|
+
console.log(chalk.yellow("š¦ Installing Next.js additional dependencies..."))
|
|
203
|
+
runCommand("npm install", targetDir)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module.exports = (program) => {
|
|
207
|
+
program
|
|
208
|
+
.command("create")
|
|
209
|
+
.description("Generate a standardized project with industry folders and NexForge libraries")
|
|
210
|
+
.action(async () => {
|
|
211
|
+
console.log(chalk.cyan("⨠Welcome to NexForge Project Generator!\n"))
|
|
212
|
+
|
|
213
|
+
const answers = await inquirer.prompt([
|
|
214
|
+
{
|
|
215
|
+
type: "input",
|
|
216
|
+
name: "projectName",
|
|
217
|
+
message: "What is your project name?",
|
|
218
|
+
validate: (input) => (input ? true : "Project name cannot be empty!"),
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
type: "list",
|
|
222
|
+
name: "template",
|
|
223
|
+
message: "Which stack would you like to use?",
|
|
224
|
+
choices: [
|
|
225
|
+
"MERN Stack (Fullstack with /frontend and /backend)",
|
|
226
|
+
"Next.js (App Router)",
|
|
227
|
+
"Frontend Only (React + Vite)",
|
|
228
|
+
"Backend Only (Node + Express)",
|
|
229
|
+
],
|
|
230
|
+
},
|
|
231
|
+
])
|
|
232
|
+
|
|
233
|
+
const { projectName, template } = answers
|
|
234
|
+
const targetDir = path.join(process.cwd(), projectName)
|
|
235
|
+
|
|
236
|
+
if (fs.existsSync(targetDir)) {
|
|
237
|
+
console.log(chalk.red(`\nā Directory ${projectName} already exists. Please choose a different name.`))
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
fs.mkdirSync(targetDir, { recursive: true })
|
|
242
|
+
|
|
243
|
+
if (template.includes("Backend Only")) {
|
|
244
|
+
setupBackend(targetDir)
|
|
245
|
+
} else if (template.includes("Frontend Only")) {
|
|
246
|
+
setupFrontend(targetDir)
|
|
247
|
+
} else if (template.includes("Next.js")) {
|
|
248
|
+
setupNextJs(targetDir)
|
|
249
|
+
} else if (template.includes("MERN Stack")) {
|
|
250
|
+
console.log(chalk.magenta("\nš„ Generating MERN Stack..."))
|
|
251
|
+
setupBackend(path.join(targetDir, "backend"))
|
|
252
|
+
setupFrontend(path.join(targetDir, "frontend"))
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
console.log(chalk.green(`\nš Project ${projectName} created successfully!`))
|
|
256
|
+
console.log(chalk.white(`\nš Next steps:`))
|
|
257
|
+
console.log(chalk.cyan(` cd ${projectName}`))
|
|
258
|
+
console.log(chalk.cyan(` nexforge init (to link with NexForge Platform)`))
|
|
259
|
+
console.log(chalk.cyan(` nexforge deploy\n`))
|
|
260
|
+
})
|
|
261
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const fs = require("fs")
|
|
2
|
+
const path = require("path")
|
|
3
|
+
const chalk = require("chalk")
|
|
4
|
+
const ora = require("ora")
|
|
5
|
+
const archiver = require("archiver")
|
|
6
|
+
const FormData = require("form-data")
|
|
7
|
+
const { axios, API_BASE_URL, CONFIG_FILE } = require("../config")
|
|
8
|
+
|
|
9
|
+
module.exports = (program) => {
|
|
10
|
+
program
|
|
11
|
+
.command("deploy")
|
|
12
|
+
.description("Deploy the current directory to NexForge")
|
|
13
|
+
.action(async () => {
|
|
14
|
+
// First we check if they are logged in by reading the config file
|
|
15
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
16
|
+
console.log(
|
|
17
|
+
chalk.red(
|
|
18
|
+
"ā You are not logged in. Please run `nexforge login` first.",
|
|
19
|
+
),
|
|
20
|
+
)
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"))
|
|
25
|
+
const projectId = config.projectId
|
|
26
|
+
|
|
27
|
+
const spinner = ora("Packaging your project...").start()
|
|
28
|
+
const zipPath = path.join(process.cwd(), "nexforge-build.zip")
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// We create a write stream to zip the contents of the current folder
|
|
32
|
+
const output = fs.createWriteStream(zipPath)
|
|
33
|
+
const archive = archiver("zip", { zlib: { level: 9 } }) // Maximum compression
|
|
34
|
+
|
|
35
|
+
// We need to wait for the archive to finish writing
|
|
36
|
+
const archivePromise = new Promise((resolve, reject) => {
|
|
37
|
+
output.on("close", resolve)
|
|
38
|
+
archive.on("error", reject)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
archive.pipe(output)
|
|
42
|
+
|
|
43
|
+
// We grab everything in the current directory EXCEPT node_modules and hidden files
|
|
44
|
+
// Skipping node_modules is super important otherwise the upload would take forever
|
|
45
|
+
archive.glob("**/*", {
|
|
46
|
+
cwd: process.cwd(),
|
|
47
|
+
ignore: ["node_modules/**", ".git/**", "nexforge-build.zip", ".env"],
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
await archive.finalize()
|
|
51
|
+
await archivePromise
|
|
52
|
+
|
|
53
|
+
spinner.text = "Uploading to NexForge servers..."
|
|
54
|
+
|
|
55
|
+
// Now we prepare the multipart form-data request to send the zip file
|
|
56
|
+
const formData = new FormData()
|
|
57
|
+
formData.append("projectZip", fs.createReadStream(zipPath))
|
|
58
|
+
|
|
59
|
+
const response = await axios.post(
|
|
60
|
+
`${API_BASE_URL}/cli/deploy/${projectId}`,
|
|
61
|
+
formData,
|
|
62
|
+
{
|
|
63
|
+
headers: {
|
|
64
|
+
...formData.getHeaders(),
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
spinner.succeed(chalk.green("š Deployment queued successfully!"))
|
|
70
|
+
console.log(chalk.cyan(`Live URL: ${response.data.liveUrl}`))
|
|
71
|
+
console.log(chalk.gray(`Streaming live build logs...\n`))
|
|
72
|
+
|
|
73
|
+
// Automatically stream logs after deployment
|
|
74
|
+
const io = require("socket.io-client")
|
|
75
|
+
const socket = io(API_BASE_URL.replace("/api", ""), {
|
|
76
|
+
transports: ["websocket", "polling"],
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
socket.emit("joinProject", projectId)
|
|
80
|
+
|
|
81
|
+
socket.on("new-log", (entry) => {
|
|
82
|
+
const color =
|
|
83
|
+
entry.level === "ERROR"
|
|
84
|
+
? chalk.red
|
|
85
|
+
: entry.level === "WARN"
|
|
86
|
+
? chalk.yellow
|
|
87
|
+
: chalk.white
|
|
88
|
+
console.log(
|
|
89
|
+
color(
|
|
90
|
+
`[${new Date(entry.timestamp).toLocaleTimeString()}] ${entry.message}`,
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
socket.on("status-change", (data) => {
|
|
96
|
+
if (data.status === "LIVE") {
|
|
97
|
+
console.log(chalk.green(`\nā
Deployment successful and Live!`))
|
|
98
|
+
socket.disconnect()
|
|
99
|
+
process.exit(0)
|
|
100
|
+
} else if (data.status === "FAILED") {
|
|
101
|
+
console.log(chalk.red(`\nā Deployment failed!`))
|
|
102
|
+
socket.disconnect()
|
|
103
|
+
process.exit(1)
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
} catch (error) {
|
|
107
|
+
spinner.fail(chalk.red("Deployment failed!"))
|
|
108
|
+
|
|
109
|
+
// If our API returned a specific error message we want to show that to the user
|
|
110
|
+
if (error.response && error.response.data) {
|
|
111
|
+
console.log(
|
|
112
|
+
chalk.red(`Server Error: ${JSON.stringify(error.response.data)}`),
|
|
113
|
+
)
|
|
114
|
+
} else {
|
|
115
|
+
console.error(error.message)
|
|
116
|
+
}
|
|
117
|
+
} finally {
|
|
118
|
+
// Clean up the temporary zip file so we don't leave trash on the user's PC
|
|
119
|
+
if (fs.existsSync(zipPath)) {
|
|
120
|
+
fs.unlinkSync(zipPath)
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
})
|
|
124
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const fs = require("fs")
|
|
2
|
+
const path = require("path")
|
|
3
|
+
const chalk = require("chalk")
|
|
4
|
+
const ora = require("ora")
|
|
5
|
+
const { axios, API_BASE_URL, CONFIG_FILE } = require("../config")
|
|
6
|
+
|
|
7
|
+
module.exports = (program) => {
|
|
8
|
+
const envCommand = program
|
|
9
|
+
.command("env")
|
|
10
|
+
.description("Manage your environment variables")
|
|
11
|
+
|
|
12
|
+
envCommand
|
|
13
|
+
.command("push")
|
|
14
|
+
.description("Push local .env file to NexForge")
|
|
15
|
+
.action(async () => {
|
|
16
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
17
|
+
console.log(
|
|
18
|
+
chalk.red(
|
|
19
|
+
"ā You are not logged in. Please run `nexforge login` first.",
|
|
20
|
+
),
|
|
21
|
+
)
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const envPath = path.join(process.cwd(), ".env")
|
|
26
|
+
if (!fs.existsSync(envPath)) {
|
|
27
|
+
console.log(chalk.red("ā No .env file found in the current directory."))
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"))
|
|
32
|
+
const projectId = config.projectId
|
|
33
|
+
|
|
34
|
+
const spinner = ora("Pushing environment variables...").start()
|
|
35
|
+
try {
|
|
36
|
+
const dotenv = require("dotenv")
|
|
37
|
+
const envConfig = dotenv.parse(fs.readFileSync(envPath))
|
|
38
|
+
|
|
39
|
+
const envs = Object.keys(envConfig).map((key) => ({
|
|
40
|
+
key,
|
|
41
|
+
value: envConfig[key],
|
|
42
|
+
}))
|
|
43
|
+
|
|
44
|
+
await axios.post(`${API_BASE_URL}/cli/env/push/${projectId}`, { envs })
|
|
45
|
+
|
|
46
|
+
spinner.succeed(
|
|
47
|
+
chalk.green("š Environment variables pushed successfully!"),
|
|
48
|
+
)
|
|
49
|
+
} catch (error) {
|
|
50
|
+
spinner.fail(chalk.red("Failed to push environment variables!"))
|
|
51
|
+
console.error(error.message)
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
envCommand
|
|
56
|
+
.command("pull")
|
|
57
|
+
.description("Pull environment variables from NexForge to local .env")
|
|
58
|
+
.action(async () => {
|
|
59
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
60
|
+
console.log(
|
|
61
|
+
chalk.red(
|
|
62
|
+
"ā You are not logged in. Please run `nexforge login` first.",
|
|
63
|
+
),
|
|
64
|
+
)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"))
|
|
69
|
+
const projectId = config.projectId
|
|
70
|
+
|
|
71
|
+
const spinner = ora("Pulling environment variables...").start()
|
|
72
|
+
try {
|
|
73
|
+
const response = await axios.get(
|
|
74
|
+
`${API_BASE_URL}/cli/env/pull/${projectId}`,
|
|
75
|
+
)
|
|
76
|
+
const envs = response.data.envs
|
|
77
|
+
|
|
78
|
+
if (!envs || envs.length === 0) {
|
|
79
|
+
spinner.info(
|
|
80
|
+
chalk.yellow("No environment variables found on the server."),
|
|
81
|
+
)
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let envContent = ""
|
|
86
|
+
envs.forEach((e) => {
|
|
87
|
+
envContent += `${e.key}=${e.value}\n`
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const envPath = path.join(process.cwd(), ".env")
|
|
91
|
+
fs.writeFileSync(envPath, envContent)
|
|
92
|
+
|
|
93
|
+
spinner.succeed(
|
|
94
|
+
chalk.green("š Environment variables pulled successfully!"),
|
|
95
|
+
)
|
|
96
|
+
console.log(chalk.cyan(`Saved to ${envPath}`))
|
|
97
|
+
} catch (error) {
|
|
98
|
+
spinner.fail(chalk.red("Failed to pull environment variables!"))
|
|
99
|
+
console.error(error.message)
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
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("init")
|
|
9
|
+
.description("Initialize a new NexForge project in the current directory")
|
|
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 set up a new NexForge project!"))
|
|
17
|
+
|
|
18
|
+
readline.question(
|
|
19
|
+
"What is your project name? (e.g. my-app): ",
|
|
20
|
+
async (projectName) => {
|
|
21
|
+
if (!projectName) {
|
|
22
|
+
console.log(chalk.red("Project name cannot be empty!"))
|
|
23
|
+
readline.close()
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
readline.question(
|
|
28
|
+
"What framework are you using? (React, Vue, Express, Node, Next.js): ",
|
|
29
|
+
async (framework) => {
|
|
30
|
+
if (!framework) {
|
|
31
|
+
console.log(chalk.red("Framework cannot be empty!"))
|
|
32
|
+
readline.close()
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const spinner = ora(
|
|
37
|
+
"Creating project on NexForge servers...",
|
|
38
|
+
).start()
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const response = await axios.post(`${API_BASE_URL}/cli/init`, {
|
|
42
|
+
projectName,
|
|
43
|
+
framework,
|
|
44
|
+
projectType: ["express", "node"].includes(
|
|
45
|
+
framework.toLowerCase(),
|
|
46
|
+
)
|
|
47
|
+
? "NODE"
|
|
48
|
+
: "STATIC",
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const { projectId } = response.data
|
|
52
|
+
|
|
53
|
+
// Ensure the config directory exists before saving
|
|
54
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
55
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// We preserve the existing CLI Token if it exists, and just update the projectId
|
|
59
|
+
let currentConfig = {}
|
|
60
|
+
if (fs.existsSync(CONFIG_FILE)) {
|
|
61
|
+
currentConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf8"))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fs.writeFileSync(
|
|
65
|
+
CONFIG_FILE,
|
|
66
|
+
JSON.stringify({ ...currentConfig, projectId }),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
spinner.succeed(chalk.green("š Project created successfully!"))
|
|
70
|
+
console.log(
|
|
71
|
+
chalk.cyan(`Project ID: ${projectId} has been saved locally.`),
|
|
72
|
+
)
|
|
73
|
+
console.log(
|
|
74
|
+
chalk.yellow(
|
|
75
|
+
`You can now run \`nexforge deploy\` to push your code.`,
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
} catch (error) {
|
|
79
|
+
spinner.fail(chalk.red("Failed to create project!"))
|
|
80
|
+
if (error.response && error.response.data) {
|
|
81
|
+
console.log(
|
|
82
|
+
chalk.red(
|
|
83
|
+
`Server Error: ${JSON.stringify(error.response.data)}`,
|
|
84
|
+
),
|
|
85
|
+
)
|
|
86
|
+
} else {
|
|
87
|
+
console.error(error.message)
|
|
88
|
+
}
|
|
89
|
+
} finally {
|
|
90
|
+
readline.close()
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
})
|
|
97
|
+
}
|
|
@@ -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
|
+
}
|