easy-devops 0.1.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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +325 -0
  3. package/cli/index.js +91 -0
  4. package/cli/managers/domain-manager.js +451 -0
  5. package/cli/managers/nginx-manager.js +329 -0
  6. package/cli/managers/node-manager.js +275 -0
  7. package/cli/managers/ssl-manager.js +397 -0
  8. package/cli/menus/.gitkeep +0 -0
  9. package/cli/menus/dashboard.js +223 -0
  10. package/cli/menus/domains.js +5 -0
  11. package/cli/menus/nginx.js +5 -0
  12. package/cli/menus/nodejs.js +5 -0
  13. package/cli/menus/settings.js +83 -0
  14. package/cli/menus/ssl.js +5 -0
  15. package/core/config.js +37 -0
  16. package/core/db.js +30 -0
  17. package/core/detector.js +257 -0
  18. package/core/nginx-conf-generator.js +309 -0
  19. package/core/shell.js +151 -0
  20. package/dashboard/lib/.gitkeep +0 -0
  21. package/dashboard/lib/cert-reader.js +59 -0
  22. package/dashboard/lib/domains-db.js +51 -0
  23. package/dashboard/lib/nginx-conf-generator.js +16 -0
  24. package/dashboard/lib/nginx-service.js +282 -0
  25. package/dashboard/public/js/app.js +486 -0
  26. package/dashboard/routes/.gitkeep +0 -0
  27. package/dashboard/routes/auth.js +30 -0
  28. package/dashboard/routes/domains.js +300 -0
  29. package/dashboard/routes/nginx.js +151 -0
  30. package/dashboard/routes/settings.js +78 -0
  31. package/dashboard/routes/ssl.js +105 -0
  32. package/dashboard/server.js +79 -0
  33. package/dashboard/views/index.ejs +327 -0
  34. package/dashboard/views/partials/domain-form.ejs +229 -0
  35. package/dashboard/views/partials/domains-panel.ejs +66 -0
  36. package/dashboard/views/partials/login.ejs +50 -0
  37. package/dashboard/views/partials/nginx-panel.ejs +90 -0
  38. package/dashboard/views/partials/overview.ejs +67 -0
  39. package/dashboard/views/partials/settings-panel.ejs +37 -0
  40. package/dashboard/views/partials/sidebar.ejs +45 -0
  41. package/dashboard/views/partials/ssl-panel.ejs +53 -0
  42. package/data/.gitkeep +0 -0
  43. package/install.bat +41 -0
  44. package/install.ps1 +653 -0
  45. package/install.sh +452 -0
  46. package/lib/installer/.gitkeep +0 -0
  47. package/lib/installer/detect.sh +88 -0
  48. package/lib/installer/node-versions.sh +109 -0
  49. package/lib/installer/nvm-bootstrap.sh +77 -0
  50. package/lib/installer/picker.sh +163 -0
  51. package/lib/installer/progress.sh +25 -0
  52. package/package.json +67 -0
@@ -0,0 +1,329 @@
1
+ /**
2
+ * cli/managers/nginx-manager.js
3
+ *
4
+ * Nginx Manager — full control over nginx from the CLI.
5
+ *
6
+ * Exported functions:
7
+ * - showNginxManager() — interactive menu for managing nginx
8
+ *
9
+ * All shell calls go through core/shell.js (run / runLive).
10
+ * Platform differences (Windows/Linux) are handled via isWindows guards.
11
+ */
12
+
13
+ import chalk from 'chalk';
14
+ import inquirer from 'inquirer';
15
+ import ora from 'ora';
16
+ import { run, runLive } from '../../core/shell.js';
17
+ import { loadConfig } from '../../core/config.js';
18
+
19
+ const isWindows = process.platform === 'win32';
20
+
21
+ // ─── getNginxStatus ───────────────────────────────────────────────────────────
22
+
23
+ async function getNginxStatus(nginxDir) {
24
+ const nginxExe = isWindows ? `${nginxDir}\\nginx.exe` : 'nginx';
25
+ const logPath = isWindows
26
+ ? `${nginxDir}\\logs\\error.log`
27
+ : '/var/log/nginx/error.log';
28
+
29
+ let running = false;
30
+ let version = null;
31
+
32
+ if (isWindows) {
33
+ const taskResult = await run('tasklist /FI "IMAGENAME eq nginx.exe" /NH');
34
+ running = taskResult.success && taskResult.stdout.toLowerCase().includes('nginx.exe');
35
+
36
+ const versionResult = await run(`& "${nginxExe}" -v`);
37
+ const versionMatch = (versionResult.stderr + versionResult.stdout).match(/nginx\/(\S+)/);
38
+ if (versionMatch) version = versionMatch[1];
39
+ } else {
40
+ const pgrepResult = await run('pgrep -x nginx');
41
+ running = pgrepResult.exitCode === 0;
42
+
43
+ const versionResult = await run('nginx -v');
44
+ const versionMatch = (versionResult.stderr + versionResult.stdout).match(/nginx\/(\S+)/);
45
+ if (versionMatch) version = versionMatch[1];
46
+ }
47
+
48
+ return { running, version, nginxDir, nginxExe, logPath };
49
+ }
50
+
51
+ // ─── testConfig ───────────────────────────────────────────────────────────────
52
+
53
+ async function testConfig(nginxExe, nginxDir) {
54
+ const cmd = isWindows ? `& "${nginxExe}" -t` : 'nginx -t';
55
+ const result = await run(cmd, { cwd: nginxDir });
56
+ return {
57
+ success: result.success,
58
+ output: (result.stdout + '\n' + result.stderr).trim(),
59
+ };
60
+ }
61
+
62
+ // ─── reloadNginx ──────────────────────────────────────────────────────────────
63
+
64
+ async function reloadNginx(nginxExe, nginxDir) {
65
+ const spinner = ora('Testing config…').start();
66
+ const configTest = await testConfig(nginxExe, nginxDir);
67
+
68
+ if (!configTest.success) {
69
+ spinner.fail('Config test failed');
70
+ console.log(chalk.red('\n' + configTest.output));
71
+ return { success: false, message: 'Config test failed', output: configTest.output };
72
+ }
73
+
74
+ spinner.text = 'Reloading nginx…';
75
+ const cmd = isWindows ? `& "${nginxExe}" -s reload` : 'systemctl reload nginx';
76
+ const result = await run(cmd, { cwd: nginxDir });
77
+
78
+ if (result.success) {
79
+ spinner.succeed('nginx reloaded successfully');
80
+ } else {
81
+ spinner.fail('Reload failed');
82
+ }
83
+
84
+ return {
85
+ success: result.success,
86
+ message: result.success ? 'nginx reloaded successfully' : 'Reload failed',
87
+ output: (result.stdout + '\n' + result.stderr).trim(),
88
+ };
89
+ }
90
+
91
+ // ─── restartNginx ─────────────────────────────────────────────────────────────
92
+
93
+ async function restartNginx(nginxExe, nginxDir) {
94
+ const spinner = ora('Testing config…').start();
95
+ const configTest = await testConfig(nginxExe, nginxDir);
96
+
97
+ if (!configTest.success) {
98
+ spinner.fail('Config test failed');
99
+ console.log(chalk.red('\n' + configTest.output));
100
+ return { success: false, message: 'Config test failed', output: configTest.output };
101
+ }
102
+
103
+ spinner.text = 'Restarting nginx…';
104
+ let result;
105
+ if (isWindows) {
106
+ await run('taskkill /f /IM nginx.exe', { cwd: nginxDir });
107
+ result = await run(`& "${nginxExe}"`, { cwd: nginxDir });
108
+ } else {
109
+ result = await run('systemctl restart nginx');
110
+ }
111
+
112
+ if (result.success) {
113
+ spinner.succeed('nginx restarted successfully');
114
+ } else {
115
+ spinner.fail('Restart failed');
116
+ }
117
+
118
+ return {
119
+ success: result.success,
120
+ message: result.success ? 'nginx restarted successfully' : 'Restart failed',
121
+ output: (result.stdout + '\n' + result.stderr).trim(),
122
+ };
123
+ }
124
+
125
+ // ─── viewErrorLog ─────────────────────────────────────────────────────────────
126
+
127
+ async function viewErrorLog(logPath) {
128
+ const cmd = isWindows
129
+ ? `Get-Content -Tail 50 "${logPath}"`
130
+ : `tail -n 50 "${logPath}"`;
131
+
132
+ const result = await run(cmd);
133
+
134
+ if (result.success && result.stdout) {
135
+ console.log('\n' + result.stdout);
136
+ } else {
137
+ console.log(chalk.yellow('\n No errors logged yet (log file not found or empty)\n'));
138
+ }
139
+ }
140
+
141
+ // ─── startNginx ───────────────────────────────────────────────────────────────
142
+
143
+ async function startNginx(nginxExe, nginxDir) {
144
+ // Step 1: config check before attempting to start
145
+ const spinner = ora('Checking config…').start();
146
+ const configTest = await testConfig(nginxExe, nginxDir);
147
+ if (!configTest.success) {
148
+ spinner.fail('Config test failed — fix the errors below before starting');
149
+ console.log(chalk.red('\n' + configTest.output));
150
+ return { success: false };
151
+ }
152
+
153
+ // Step 2: launch nginx
154
+ spinner.text = 'Starting nginx…';
155
+
156
+ if (isWindows) {
157
+ // nginx.exe runs in the foreground on Windows and never exits, so we must
158
+ // use Start-Process to launch it as a detached background process.
159
+ const startCmd = `Start-Process -FilePath "${nginxExe}" -WorkingDirectory "${nginxDir}" -WindowStyle Hidden`;
160
+ await run(startCmd, { timeout: 10000 });
161
+ } else {
162
+ const result = await run('systemctl start nginx', { timeout: 15000 });
163
+ if (!result.success) {
164
+ spinner.fail('Start failed');
165
+ console.log(chalk.red('\n' + (result.stderr || result.stdout)));
166
+ return { success: false };
167
+ }
168
+ }
169
+
170
+ // Step 3: wait briefly then verify nginx is actually running
171
+ await new Promise(r => setTimeout(r, 1500));
172
+
173
+ let running = false;
174
+ if (isWindows) {
175
+ const check = await run('tasklist /FI "IMAGENAME eq nginx.exe" /NH');
176
+ running = check.success && check.stdout.toLowerCase().includes('nginx.exe');
177
+ } else {
178
+ const check = await run('pgrep -x nginx');
179
+ running = check.exitCode === 0;
180
+ }
181
+
182
+ if (running) {
183
+ spinner.succeed('nginx started successfully');
184
+ return { success: true };
185
+ }
186
+
187
+ // Step 4: nginx didn't come up — read the error log to surface the reason
188
+ spinner.fail('nginx did not start');
189
+ const logPath = isWindows
190
+ ? `${nginxDir}\\logs\\error.log`
191
+ : '/var/log/nginx/error.log';
192
+ const logCmd = isWindows
193
+ ? `Get-Content -Tail 20 "${logPath}" -ErrorAction SilentlyContinue`
194
+ : `tail -n 20 "${logPath}" 2>/dev/null`;
195
+ const logResult = await run(logCmd);
196
+
197
+ console.log(chalk.yellow('\n Recent error log:'));
198
+ if (logResult.success && logResult.stdout) {
199
+ console.log(chalk.red(logResult.stdout));
200
+ } else {
201
+ console.log(chalk.gray(' (error log not found or empty)'));
202
+ }
203
+
204
+ return { success: false };
205
+ }
206
+
207
+ // ─── stopNginx ────────────────────────────────────────────────────────────────
208
+
209
+ async function stopNginx(nginxDir) {
210
+ const spinner = ora('Stopping nginx…').start();
211
+ const cmd = isWindows ? 'taskkill /f /IM nginx.exe' : 'systemctl stop nginx';
212
+ const result = await run(cmd, { cwd: nginxDir });
213
+ if (result.success) {
214
+ spinner.succeed('nginx stopped');
215
+ } else {
216
+ spinner.fail('Stop failed');
217
+ console.log(chalk.red('\n' + (result.stderr || result.stdout)));
218
+ }
219
+ return { success: result.success, output: (result.stdout + '\n' + result.stderr).trim() };
220
+ }
221
+
222
+ // ─── installNginx ─────────────────────────────────────────────────────────────
223
+
224
+ async function installNginx() {
225
+ const spinner = ora('Installing nginx…').start();
226
+
227
+ const cmd = isWindows
228
+ ? 'winget install -e --id Nginx.Nginx --accept-package-agreements --accept-source-agreements'
229
+ : 'sudo apt-get install -y nginx';
230
+
231
+ const result = await run(cmd, { timeout: 120000 });
232
+
233
+ if (result.success) {
234
+ spinner.succeed('nginx installed successfully');
235
+ return { success: true, message: 'nginx installed successfully', output: result.stdout };
236
+ } else {
237
+ spinner.fail('Installation failed');
238
+ console.log(chalk.red(result.stderr || result.stdout));
239
+ console.log(chalk.gray('\n Manual instructions: https://nginx.org/en/docs/install.html\n'));
240
+ return { success: false, message: 'Installation failed', output: result.stderr || result.stdout };
241
+ }
242
+ }
243
+
244
+ // ─── showNginxManager ─────────────────────────────────────────────────────────
245
+
246
+ export async function showNginxManager() {
247
+ while (true) {
248
+ const { nginxDir } = loadConfig();
249
+ const status = await getNginxStatus(nginxDir);
250
+
251
+ console.log(chalk.bold('\n Nginx Manager'));
252
+ console.log(chalk.gray(' ' + '─'.repeat(40)));
253
+
254
+ if (status.version) {
255
+ const statusIcon = status.running ? chalk.green('✅ Running') : chalk.red('❌ Stopped');
256
+ console.log(` ${statusIcon} | v${status.version} | ${status.nginxDir}`);
257
+ } else {
258
+ console.log(` ${chalk.yellow('⚠ Not installed')}`);
259
+ }
260
+ console.log();
261
+
262
+ const choices = [];
263
+
264
+ if (status.running) {
265
+ // nginx is running — offer reload, restart, stop
266
+ choices.push('Reload nginx', 'Restart nginx', 'Stop nginx', new inquirer.Separator());
267
+ choices.push('Test config', 'View error log', new inquirer.Separator());
268
+ } else if (status.version) {
269
+ // nginx is installed but stopped — offer start
270
+ choices.push('Start nginx', new inquirer.Separator());
271
+ choices.push('Test config', 'View error log', new inquirer.Separator());
272
+ } else {
273
+ // nginx not found — offer install
274
+ choices.push('Install nginx', new inquirer.Separator());
275
+ }
276
+
277
+ choices.push('← Back');
278
+
279
+ let choice;
280
+ try {
281
+ ({ choice } = await inquirer.prompt([{
282
+ type: 'list',
283
+ name: 'choice',
284
+ message: 'Select an option:',
285
+ choices,
286
+ }]));
287
+ } catch (err) {
288
+ if (err.name === 'ExitPromptError') return;
289
+ throw err;
290
+ }
291
+
292
+ switch (choice) {
293
+ case 'Reload nginx':
294
+ await reloadNginx(status.nginxExe, status.nginxDir);
295
+ break;
296
+ case 'Restart nginx':
297
+ await restartNginx(status.nginxExe, status.nginxDir);
298
+ break;
299
+ case 'Start nginx':
300
+ await startNginx(status.nginxExe, status.nginxDir);
301
+ break;
302
+ case 'Stop nginx':
303
+ await stopNginx(status.nginxDir);
304
+ break;
305
+ case 'Test config': {
306
+ const result = await testConfig(status.nginxExe, status.nginxDir);
307
+ console.log(
308
+ '\n Config test: ' +
309
+ (result.success ? chalk.green('✓ OK') : chalk.red('✗ Failed')) +
310
+ '\n',
311
+ );
312
+ console.log(chalk.gray(result.output) + '\n');
313
+ break;
314
+ }
315
+ case 'View error log':
316
+ await viewErrorLog(status.logPath);
317
+ break;
318
+ case 'Install nginx':
319
+ await installNginx();
320
+ break;
321
+ case '← Back':
322
+ return;
323
+ }
324
+
325
+ if (choice !== '← Back') {
326
+ await inquirer.prompt([{ type: 'input', name: '_', message: 'Press Enter to continue...' }]);
327
+ }
328
+ }
329
+ }
@@ -0,0 +1,275 @@
1
+ /**
2
+ * cli/managers/node-manager.js
3
+ *
4
+ * Node.js Manager — full control over Node.js from the CLI.
5
+ *
6
+ * Exported functions:
7
+ * - showNodeManager() — interactive menu for managing Node.js
8
+ *
9
+ * All shell calls go through core/shell.js (run / runLive).
10
+ * On Windows, nvm-windows syntax is used where it differs from nvm.
11
+ */
12
+
13
+ import chalk from 'chalk';
14
+ import inquirer from 'inquirer';
15
+ import { run, runLive } from '../../core/shell.js';
16
+
17
+ const isWindows = process.platform === 'win32';
18
+
19
+ // ─── getCurrentVersions ───────────────────────────────────────────────────────
20
+
21
+ async function getCurrentVersions() {
22
+ const [nodeRes, npmRes] = await Promise.all([
23
+ run('node -v'),
24
+ run('npm -v'),
25
+ ]);
26
+ return {
27
+ node: nodeRes.success ? nodeRes.stdout : 'unknown',
28
+ npm: npmRes.success ? npmRes.stdout : 'unknown',
29
+ };
30
+ }
31
+
32
+ // ─── switchVersion ────────────────────────────────────────────────────────────
33
+
34
+ async function switchVersion() {
35
+ // Verify nvm is present
36
+ const nvmCheck = await run(isWindows ? 'nvm version' : 'nvm --version');
37
+ if (!nvmCheck.success) {
38
+ console.log(chalk.yellow('\n nvm not installed.'));
39
+ if (isWindows) {
40
+ console.log(chalk.gray(' Re-run install.bat and choose to install nvm-windows,'));
41
+ console.log(chalk.gray(' or download it from: https://github.com/coreybutler/nvm-windows/releases'));
42
+ } else {
43
+ console.log(chalk.gray(' Install nvm: https://github.com/nvm-sh/nvm#installing-and-updating'));
44
+ }
45
+ console.log();
46
+ await inquirer.prompt([{ type: 'input', name: '_', message: 'Press Enter to continue...' }]);
47
+ return;
48
+ }
49
+
50
+ console.log(chalk.gray('\n Fetching available Node.js versions…'));
51
+
52
+ let versions = [];
53
+
54
+ if (isWindows) {
55
+ // nvm-windows: `nvm list available` outputs a formatted table
56
+ const result = await run('nvm list available', { timeout: 60000 });
57
+ if (result.success && result.stdout) {
58
+ const versionRegex = /\b(\d+\.\d+\.\d+)\b/g;
59
+ const seen = new Set();
60
+ for (const line of result.stdout.split('\n')) {
61
+ let match;
62
+ while ((match = versionRegex.exec(line)) !== null) {
63
+ if (!seen.has(match[1])) {
64
+ seen.add(match[1]);
65
+ versions.push(match[1]);
66
+ }
67
+ }
68
+ }
69
+ }
70
+ } else {
71
+ // nvm (Unix): `nvm ls-remote --lts`
72
+ const result = await run(
73
+ 'export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; nvm ls-remote --lts',
74
+ { timeout: 60000 },
75
+ );
76
+ if (result.success && result.stdout) {
77
+ for (const line of result.stdout.split('\n')) {
78
+ const match = line.match(/v(\d+\.\d+\.\d+)/);
79
+ if (match) versions.push(match[1]);
80
+ }
81
+ versions.reverse(); // newest first
82
+ }
83
+ }
84
+
85
+ if (versions.length === 0) {
86
+ console.log(chalk.red('\n Could not fetch available versions. Check your internet connection.\n'));
87
+ await inquirer.prompt([{ type: 'input', name: '_', message: 'Press Enter to continue...' }]);
88
+ return;
89
+ }
90
+
91
+ const displayVersions = versions.slice(0, 30).map(v => `v${v}`);
92
+ displayVersions.push('← Cancel');
93
+
94
+ const { selected } = await inquirer.prompt([{
95
+ type: 'list',
96
+ name: 'selected',
97
+ message: 'Select Node.js version to switch to:',
98
+ choices: displayVersions,
99
+ pageSize: 15,
100
+ }]);
101
+
102
+ if (selected === '← Cancel') return;
103
+
104
+ const version = selected.replace(/^v/, '');
105
+ console.log(chalk.cyan(`\n Switching to Node.js ${selected}…\n`));
106
+
107
+ const switchCmd = isWindows
108
+ ? `nvm use ${version}`
109
+ : `export NVM_DIR="$HOME/.nvm"; [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"; nvm use ${version}`;
110
+
111
+ await runLive(switchCmd, { timeout: 60000 });
112
+
113
+ await inquirer.prompt([{ type: 'input', name: '_', message: 'Press Enter to continue...' }]);
114
+ }
115
+
116
+ // ─── listGlobalPackages ───────────────────────────────────────────────────────
117
+
118
+ async function listGlobalPackages() {
119
+ console.log(chalk.cyan('\n Global npm packages:\n'));
120
+ const result = await run('npm list -g --depth=0');
121
+ if (result.stdout) {
122
+ console.log(result.stdout);
123
+ } else {
124
+ console.log(chalk.red(' Failed to list global packages.'));
125
+ if (result.stderr) console.log(chalk.gray(' ' + result.stderr));
126
+ }
127
+ await inquirer.prompt([{ type: 'input', name: '_', message: 'Press Enter to continue...' }]);
128
+ }
129
+
130
+ // ─── installGlobalPackage ─────────────────────────────────────────────────────
131
+
132
+ async function installGlobalPackage() {
133
+ const { packageName } = await inquirer.prompt([{
134
+ type: 'input',
135
+ name: 'packageName',
136
+ message: 'Package name to install globally:',
137
+ validate: (v) => v.trim().length > 0 || 'Package name cannot be empty',
138
+ }]);
139
+
140
+ const name = packageName.trim();
141
+ console.log(chalk.cyan(`\n Installing ${name} globally…\n`));
142
+
143
+ const exitCode = await runLive(`npm install -g ${name}`, { timeout: 120000 });
144
+
145
+ if (exitCode === 0) {
146
+ console.log(chalk.green(`\n ✓ ${name} installed successfully`));
147
+ } else {
148
+ console.log(chalk.red(`\n ✗ Installation failed (exit code ${exitCode})`));
149
+ }
150
+ await inquirer.prompt([{ type: 'input', name: '_', message: 'Press Enter to continue...' }]);
151
+ }
152
+
153
+ // ─── uninstallGlobalPackage ───────────────────────────────────────────────────
154
+
155
+ async function uninstallGlobalPackage() {
156
+ const result = await run('npm list -g --depth=0 --parseable');
157
+
158
+ const packages = (result.stdout || '')
159
+ .split('\n')
160
+ .filter(l => l.includes('node_modules'))
161
+ .map(l => l.split(/[/\\]node_modules[/\\]/).pop()?.trim())
162
+ .filter(Boolean)
163
+ .filter(p => p !== 'npm'); // protect npm itself
164
+
165
+ if (packages.length === 0) {
166
+ console.log(chalk.yellow('\n No removable global packages found.\n'));
167
+ await inquirer.prompt([{ type: 'input', name: '_', message: 'Press Enter to continue...' }]);
168
+ return;
169
+ }
170
+
171
+ const { selected } = await inquirer.prompt([{
172
+ type: 'list',
173
+ name: 'selected',
174
+ message: 'Select package to uninstall:',
175
+ choices: [...packages, '← Cancel'],
176
+ pageSize: 15,
177
+ }]);
178
+
179
+ if (selected === '← Cancel') return;
180
+
181
+ console.log(chalk.cyan(`\n Uninstalling ${selected}…\n`));
182
+
183
+ const exitCode = await runLive(`npm uninstall -g ${selected}`, { timeout: 60000 });
184
+
185
+ if (exitCode === 0) {
186
+ console.log(chalk.green(`\n ✓ ${selected} uninstalled successfully`));
187
+ } else {
188
+ console.log(chalk.red(`\n ✗ Uninstall failed (exit code ${exitCode})`));
189
+ }
190
+ await inquirer.prompt([{ type: 'input', name: '_', message: 'Press Enter to continue...' }]);
191
+ }
192
+
193
+ // ─── updateNpm ────────────────────────────────────────────────────────────────
194
+
195
+ async function updateNpm() {
196
+ console.log(chalk.cyan('\n Updating npm to latest…\n'));
197
+
198
+ const exitCode = await runLive('npm install -g npm@latest', { timeout: 120000 });
199
+
200
+ if (exitCode === 0) {
201
+ console.log(chalk.green('\n ✓ npm updated successfully'));
202
+ } else {
203
+ console.log(chalk.red(`\n ✗ Update failed (exit code ${exitCode})`));
204
+ }
205
+ await inquirer.prompt([{ type: 'input', name: '_', message: 'Press Enter to continue...' }]);
206
+ }
207
+
208
+ // ─── showNodeManager ──────────────────────────────────────────────────────────
209
+
210
+ export async function showNodeManager() {
211
+ while (true) {
212
+ const versions = await getCurrentVersions();
213
+
214
+ console.log(chalk.bold('\n Node.js Manager'));
215
+ console.log(chalk.gray(' ' + '─'.repeat(40)));
216
+ console.log(` Current: ${chalk.green('Node ' + versions.node)} | ${chalk.yellow('npm v' + versions.npm)}`);
217
+ console.log();
218
+
219
+ let choice;
220
+ try {
221
+ ({ choice } = await inquirer.prompt([{
222
+ type: 'list',
223
+ name: 'choice',
224
+ message: 'Select an option:',
225
+ choices: [
226
+ 'Switch Node version',
227
+ 'Manage global packages',
228
+ new inquirer.Separator(),
229
+ 'Update npm',
230
+ new inquirer.Separator(),
231
+ '← Back',
232
+ ],
233
+ }]));
234
+ } catch (err) {
235
+ if (err.name === 'ExitPromptError') return;
236
+ throw err;
237
+ }
238
+
239
+ switch (choice) {
240
+ case 'Switch Node version': await switchVersion(); break;
241
+ case 'Manage global packages': await manageGlobalPackages(); break;
242
+ case 'Update npm': await updateNpm(); break;
243
+ case '← Back': return;
244
+ }
245
+ }
246
+ }
247
+
248
+ // ─── manageGlobalPackages ─────────────────────────────────────────────────────
249
+
250
+ async function manageGlobalPackages() {
251
+ let choice;
252
+ try {
253
+ ({ choice } = await inquirer.prompt([{
254
+ type: 'list',
255
+ name: 'choice',
256
+ message: 'Global packages:',
257
+ choices: [
258
+ 'List global packages',
259
+ 'Install global package',
260
+ 'Uninstall global package',
261
+ '← Back',
262
+ ],
263
+ }]));
264
+ } catch (err) {
265
+ if (err.name === 'ExitPromptError') return;
266
+ throw err;
267
+ }
268
+
269
+ switch (choice) {
270
+ case 'List global packages': await listGlobalPackages(); break;
271
+ case 'Install global package': await installGlobalPackage(); break;
272
+ case 'Uninstall global package': await uninstallGlobalPackage(); break;
273
+ case '← Back': return;
274
+ }
275
+ }