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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexforge-cli",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
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": ["cli", "deployment", "nexforge"],
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,316 @@
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
+ fs.mkdirSync(targetDir, { recursive: true })
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
+ // Create Vite Boilerplate
133
+ const viteConfig = `import { defineConfig } from 'vite'
134
+ import react from '@vitejs/plugin-react'
135
+
136
+ export default defineConfig({
137
+ plugins: [react()],
138
+ })`
139
+ fs.writeFileSync(path.join(targetDir, "vite.config.js"), viteConfig)
140
+
141
+ const indexHtml = `<!doctype html>
142
+ <html lang="en">
143
+ <head>
144
+ <meta charset="UTF-8" />
145
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
146
+ <title>NexForge App</title>
147
+ </head>
148
+ <body>
149
+ <div id="root"></div>
150
+ <script type="module" src="/src/main.jsx"></script>
151
+ </body>
152
+ </html>`
153
+ fs.writeFileSync(path.join(targetDir, "index.html"), indexHtml)
154
+
155
+ const mainJsx = `import React from 'react'
156
+ import ReactDOM from 'react-dom/client'
157
+ import App from './App.jsx'
158
+ import './index.css'
159
+
160
+ ReactDOM.createRoot(document.getElementById('root')).render(
161
+ <React.StrictMode>
162
+ <App />
163
+ </React.StrictMode>,
164
+ )`
165
+ fs.writeFileSync(path.join(targetDir, "src", "main.jsx"), mainJsx)
166
+
167
+ const appJsx = `import React from 'react'
168
+
169
+ function App() {
170
+ return (
171
+ <div className="min-h-screen flex items-center justify-center bg-gray-900 text-white">
172
+ <h1 className="text-4xl font-bold">Welcome to NexForge</h1>
173
+ </div>
174
+ )
175
+ }
176
+
177
+ export default App`
178
+ fs.writeFileSync(path.join(targetDir, "src", "App.jsx"), appJsx)
179
+
180
+ // Merge package.json
181
+ const pkgJson = {
182
+ name: "frontend",
183
+ private: true,
184
+ version: "0.0.0",
185
+ type: "module",
186
+ scripts: {
187
+ dev: "vite",
188
+ build: "vite build",
189
+ preview: "vite preview"
190
+ },
191
+ dependencies: { ...FRONTEND_DEPS, "react": "^19.2.6", "react-dom": "^19.2.6", "react-router-dom": "^7.18.0" },
192
+ devDependencies: { "@vitejs/plugin-react": "^6.0.1", "vite": "^8.0.12", tailwindcss: "^3.4.19", postcss: "^8.5.15", autoprefixer: "^10.5.0" }
193
+ }
194
+ fs.writeFileSync(path.join(targetDir, "package.json"), JSON.stringify(pkgJson, null, 2))
195
+
196
+ // Tailwind config
197
+ const twConfig = `/** @type {import('tailwindcss').Config} */
198
+ export default {
199
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
200
+ theme: { extend: {} },
201
+ plugins: [],
202
+ }`
203
+ fs.writeFileSync(path.join(targetDir, "tailwind.config.js"), twConfig)
204
+ fs.writeFileSync(path.join(targetDir, "postcss.config.js"), `export default { plugins: { tailwindcss: {}, autoprefixer: {} } }`)
205
+
206
+ const css = `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n`
207
+ fs.writeFileSync(path.join(targetDir, "src", "index.css"), css)
208
+
209
+ // Utils
210
+ const utilsJs = `import { clsx } from "clsx"
211
+ import { twMerge } from "tailwind-merge"
212
+
213
+ export function cn(...inputs) {
214
+ return twMerge(clsx(inputs))
215
+ }
216
+ `
217
+ fs.writeFileSync(path.join(targetDir, "src", "lib", "utils.js"), utilsJs)
218
+
219
+ console.log(chalk.yellow("šŸ“¦ Installing frontend dependencies..."))
220
+ runCommand("npm install", targetDir)
221
+ }
222
+
223
+ const setupNextJs = (targetDir) => {
224
+ console.log(chalk.cyan("\nšŸš€ Scaffolding Next.js App Router Project..."))
225
+ // We use npx create-next-app
226
+ const command = `npx create-next-app@latest ${path.basename(targetDir)} --js --tailwind --eslint --app --src-dir --import-alias "@/*"`
227
+ runCommand(command, path.dirname(targetDir))
228
+
229
+ // Create custom folders
230
+ createFolderStructure(path.join(targetDir, "src"), [
231
+ "app/api",
232
+ "components",
233
+ "hooks",
234
+ "lib",
235
+ "store",
236
+ "services",
237
+ "utils",
238
+ ])
239
+
240
+ // Merge package.json
241
+ const pkgPath = path.join(targetDir, "package.json")
242
+ if (fs.existsSync(pkgPath)) {
243
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
244
+ pkg.dependencies = { ...pkg.dependencies, ...NEXTJS_DEPS }
245
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2))
246
+ }
247
+
248
+ const utilsJs = `import { clsx } from "clsx"
249
+ import { twMerge } from "tailwind-merge"
250
+
251
+ export function cn(...inputs) {
252
+ return twMerge(clsx(inputs))
253
+ }
254
+ `
255
+ fs.writeFileSync(path.join(targetDir, "src", "lib", "utils.js"), utilsJs)
256
+
257
+ console.log(chalk.yellow("šŸ“¦ Installing Next.js additional dependencies..."))
258
+ runCommand("npm install", targetDir)
259
+ }
260
+
261
+ module.exports = (program) => {
262
+ program
263
+ .command("create")
264
+ .description("Generate a standardized project with industry folders and NexForge libraries")
265
+ .action(async () => {
266
+ console.log(chalk.cyan("✨ Welcome to NexForge Project Generator!\n"))
267
+
268
+ const answers = await inquirer.prompt([
269
+ {
270
+ type: "input",
271
+ name: "projectName",
272
+ message: "What is your project name?",
273
+ validate: (input) => (input ? true : "Project name cannot be empty!"),
274
+ },
275
+ {
276
+ type: "list",
277
+ name: "template",
278
+ message: "Which stack would you like to use?",
279
+ choices: [
280
+ "MERN Stack (Fullstack with /frontend and /backend)",
281
+ "Next.js (App Router)",
282
+ "Frontend Only (React + Vite)",
283
+ "Backend Only (Node + Express)",
284
+ ],
285
+ },
286
+ ])
287
+
288
+ const { projectName, template } = answers
289
+ const targetDir = path.join(process.cwd(), projectName)
290
+
291
+ if (fs.existsSync(targetDir)) {
292
+ console.log(chalk.red(`\nāŒ Directory ${projectName} already exists. Please choose a different name.`))
293
+ return
294
+ }
295
+
296
+ fs.mkdirSync(targetDir, { recursive: true })
297
+
298
+ if (template.includes("Backend Only")) {
299
+ setupBackend(targetDir)
300
+ } else if (template.includes("Frontend Only")) {
301
+ setupFrontend(targetDir)
302
+ } else if (template.includes("Next.js")) {
303
+ setupNextJs(targetDir)
304
+ } else if (template.includes("MERN Stack")) {
305
+ console.log(chalk.magenta("\nšŸ”„ Generating MERN Stack..."))
306
+ setupBackend(path.join(targetDir, "backend"))
307
+ setupFrontend(path.join(targetDir, "frontend"))
308
+ }
309
+
310
+ console.log(chalk.green(`\nšŸŽ‰ Project ${projectName} created successfully!`))
311
+ console.log(chalk.white(`\nšŸ‘‰ Next steps:`))
312
+ console.log(chalk.cyan(` cd ${projectName}`))
313
+ console.log(chalk.cyan(` nexforge init (to link with NexForge Platform)`))
314
+ console.log(chalk.cyan(` nexforge deploy\n`))
315
+ })
316
+ }
@@ -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
+ }