easy-devops 0.1.2 → 0.1.4

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/cli/index.js CHANGED
@@ -11,6 +11,7 @@ import sslMenu from './menus/ssl.js';
11
11
  import domainsMenu from './menus/domains.js';
12
12
  import dashboardMenu from './menus/dashboard.js';
13
13
  import settingsMenu from './menus/settings.js';
14
+ import updateMenu from './menus/update.js';
14
15
 
15
16
  const require = createRequire(import.meta.url);
16
17
  const { version } = require('../package.json');
@@ -44,6 +45,7 @@ async function showMainMenu() {
44
45
  '🔗 Domain Manager',
45
46
  '🎛️ Open Dashboard',
46
47
  '⚙️ Settings',
48
+ '🔄 Check for Updates',
47
49
  '✖ Exit',
48
50
  ],
49
51
  }]);
@@ -58,6 +60,7 @@ async function dispatch(choice) {
58
60
  case '🔗 Domain Manager': await domainsMenu(); break;
59
61
  case '🎛️ Open Dashboard': await dashboardMenu(); break;
60
62
  case '⚙️ Settings': await settingsMenu(); break;
63
+ case '🔄 Check for Updates': await updateMenu(); break;
61
64
  case '✖ Exit': process.exit(0);
62
65
  }
63
66
  }
@@ -268,7 +268,7 @@ async function installNginx() {
268
268
  let nginxVersion = FALLBACK_VERSION;
269
269
 
270
270
  const fetchVersionResult = await run(
271
- `try { $p=(Invoke-WebRequest -Uri 'https://nginx.org/en/download.html' -UseBasicParsing -TimeoutSec 15).Content; if($p -match 'nginx-(\\d+\\.\\d+\\.\\d+)\\.zip'){$Matches[1]}else{''} } catch { '' }`,
271
+ `[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; try { $p=(Invoke-WebRequest -Uri 'https://nginx.org/en/download.html' -UseBasicParsing -TimeoutSec 15).Content; if($p -match 'nginx-(\\d+\\.\\d+\\.\\d+)\\.zip'){$Matches[1]}else{''} } catch { '' }`,
272
272
  { timeout: 20000 },
273
273
  );
274
274
  const fetched = (fetchVersionResult.stdout || '').trim();
@@ -276,15 +276,30 @@ async function installNginx() {
276
276
 
277
277
  const { nginxDir } = loadConfig();
278
278
  const zipUrl = `https://nginx.org/download/nginx-${nginxVersion}.zip`;
279
+ const zipDest = `$env:TEMP\\nginx-${nginxVersion}.zip`;
279
280
 
280
281
  spinner.text = `Downloading nginx ${nginxVersion}…`;
281
282
 
282
- const downloadResult = await run(
283
- `$ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '${zipUrl}' -OutFile "$env:TEMP\\nginx-${nginxVersion}.zip" -UseBasicParsing -TimeoutSec 120`,
283
+ // Try Invoke-WebRequest with TLS 1.2 forced, fall back to curl.exe
284
+ let downloadOk = false;
285
+ let downloadResult = await run(
286
+ `[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '${zipUrl}' -OutFile "${zipDest}" -UseBasicParsing -TimeoutSec 120`,
284
287
  { timeout: 130000 },
285
288
  );
289
+ if (downloadResult.success) {
290
+ downloadOk = true;
291
+ } else {
292
+ const hasCurl = (await run('where.exe curl.exe 2>$null')).success;
293
+ if (hasCurl) {
294
+ downloadResult = await run(
295
+ `curl.exe -L --silent --show-error --max-time 120 -o "${zipDest}" "${zipUrl}"`,
296
+ { timeout: 130000 },
297
+ );
298
+ if (downloadResult.success) downloadOk = true;
299
+ }
300
+ }
286
301
 
287
- if (!downloadResult.success) {
302
+ if (!downloadOk) {
288
303
  spinner.fail('Download failed');
289
304
  console.log(chalk.red(downloadResult.stderr || downloadResult.stdout));
290
305
  console.log(chalk.gray(`\n Download nginx manually from: https://nginx.org/en/download.html\n`));
@@ -276,18 +276,46 @@ async function installCertbot() {
276
276
  console.log(chalk.yellow(' Chocolatey failed, trying direct download...\n'));
277
277
  }
278
278
 
279
- // 3. Direct download — official EFF installer (NSIS, supports /S silent flag)
280
- const installerUrl = 'https://dl.eff.org/certbot-beta-installer-win_amd64_signed.exe';
279
+ // 3. Direct download — try multiple sources × multiple download methods
280
+ // PowerShell 5.1 on Windows Server defaults to TLS 1.0; GitHub requires TLS 1.2+.
281
+ // Force TLS 1.2 before every Invoke-WebRequest call.
282
+ // Also try curl.exe (built into Windows Server 2019+) as a second method.
283
+ const INSTALLER_FILENAME = 'certbot-beta-installer-win_amd64_signed.exe';
284
+ const INSTALLER_DEST = '$env:TEMP\\certbot-installer.exe';
285
+ const downloadSources = [
286
+ `https://github.com/certbot/certbot/releases/latest/download/${INSTALLER_FILENAME}`,
287
+ `https://dl.eff.org/${INSTALLER_FILENAME}`,
288
+ ];
289
+
290
+ const hasCurl = (await run('where.exe curl.exe 2>$null')).success;
291
+
292
+ let downloaded = false;
293
+ for (const url of downloadSources) {
294
+ const label = new URL(url).hostname;
295
+ console.log(chalk.gray(`\n Downloading certbot installer from ${label} ...\n`));
296
+
297
+ // Method A: Invoke-WebRequest with TLS 1.2 forced
298
+ const iwr = await run(
299
+ `[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '${url}' -OutFile "${INSTALLER_DEST}" -UseBasicParsing -TimeoutSec 120`,
300
+ { timeout: 130000 },
301
+ );
302
+ if (iwr.success) { downloaded = true; break; }
281
303
 
282
- console.log(chalk.gray('\n Downloading certbot installer from dl.eff.org ...\n'));
304
+ // Method B: curl.exe (handles TLS independently of PowerShell/.NET settings)
305
+ if (hasCurl) {
306
+ const curlResult = await run(
307
+ `curl.exe -L --silent --show-error --max-time 120 -o "${INSTALLER_DEST}" "${url}"`,
308
+ { timeout: 130000 },
309
+ );
310
+ if (curlResult.success) { downloaded = true; break; }
311
+ }
283
312
 
284
- const downloadResult = await run(
285
- `$ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '${installerUrl}' -OutFile "$env:TEMP\\certbot-installer.exe" -UseBasicParsing -TimeoutSec 120`,
286
- { timeout: 130000 },
287
- );
313
+ console.log(chalk.yellow(` Failed from ${label}`));
314
+ }
288
315
 
289
- if (!downloadResult.success) {
290
- console.log(chalk.red(' Download failed: ' + (downloadResult.stderr || downloadResult.stdout)));
316
+ if (!downloaded) {
317
+ console.log(chalk.red('\n Download failed from all sources.'));
318
+ console.log(chalk.gray(' Install manually: https://certbot.eff.org/instructions?ws=other&os=windows\n'));
291
319
  return { success: false };
292
320
  }
293
321
 
@@ -69,7 +69,7 @@ function isPidAlive(pid) {
69
69
 
70
70
  // ─── Status ───────────────────────────────────────────────────────────────────
71
71
 
72
- async function getDashboardStatus() {
72
+ export async function getDashboardStatus() {
73
73
  const { dashboardPort } = loadConfig();
74
74
  const storedPid = dbGet('dashboard-pid');
75
75
  const storedPort = dbGet('dashboard-port') ?? dashboardPort;
@@ -91,7 +91,7 @@ async function getDashboardStatus() {
91
91
 
92
92
  // ─── Start ────────────────────────────────────────────────────────────────────
93
93
 
94
- async function startDashboard(port) {
94
+ export async function startDashboard(port) {
95
95
  await fs.mkdir(path.dirname(LOG_PATH), { recursive: true });
96
96
 
97
97
  // openSync gives a real fd immediately — required by spawn's stdio option
@@ -118,7 +118,7 @@ async function startDashboard(port) {
118
118
 
119
119
  // ─── Stop ─────────────────────────────────────────────────────────────────────
120
120
 
121
- async function stopDashboard(pid) {
121
+ export async function stopDashboard(pid) {
122
122
  if (!pid) return { success: false };
123
123
  try {
124
124
  if (isWindows) {
@@ -0,0 +1,178 @@
1
+ /**
2
+ * cli/menus/update.js
3
+ *
4
+ * Check for updates and upgrade easy-devops in place.
5
+ *
6
+ * Flow:
7
+ * 1. Fetch latest version from npm registry
8
+ * 2. If an update is available, offer to install it
9
+ * 3. Before installing: record dashboard running state in DB
10
+ * (key: 'update-pre-dashboard') so it survives a crash mid-update
11
+ * 4. Stop dashboard if it was running
12
+ * 5. Run: npm install -g easy-devops@<latest>
13
+ * 6. Re-start dashboard if it was running before
14
+ * 7. Clear the saved state key
15
+ */
16
+
17
+ import chalk from 'chalk';
18
+ import inquirer from 'inquirer';
19
+ import ora from 'ora';
20
+ import path from 'path';
21
+ import { fileURLToPath } from 'url';
22
+ import { createRequire } from 'module';
23
+ import { run } from '../../core/shell.js';
24
+ import { dbGet, dbSet } from '../../core/db.js';
25
+ import { loadConfig } from '../../core/config.js';
26
+ import { getDashboardStatus, startDashboard, stopDashboard } from './dashboard.js';
27
+
28
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
29
+ const require = createRequire(import.meta.url);
30
+ const { version: currentVersion } = require('../../package.json');
31
+
32
+ // ─── Version helpers ──────────────────────────────────────────────────────────
33
+
34
+ async function fetchLatestVersion() {
35
+ const result = await run('npm view easy-devops version', { timeout: 20000 });
36
+ if (result.success && result.stdout.trim()) return result.stdout.trim();
37
+ return null;
38
+ }
39
+
40
+ function isNewer(latest, current) {
41
+ const parse = v => v.replace(/^v/, '').split('.').map(Number);
42
+ const [la, lb, lc] = parse(latest);
43
+ const [ca, cb, cc] = parse(current);
44
+ if (la !== ca) return la > ca;
45
+ if (lb !== cb) return lb > cb;
46
+ return lc > cc;
47
+ }
48
+
49
+ // ─── Recover interrupted update ───────────────────────────────────────────────
50
+ // If a previous update crashed after stopping the dashboard but before restarting
51
+ // it, the key 'update-pre-dashboard' is still set. Offer to restart it.
52
+
53
+ async function recoverIfNeeded() {
54
+ const saved = dbGet('update-pre-dashboard');
55
+ if (!saved?.wasRunning) return;
56
+
57
+ console.log(chalk.yellow('\n A previous update left the dashboard stopped.'));
58
+ const { restart } = await inquirer.prompt([{
59
+ type: 'confirm',
60
+ name: 'restart',
61
+ message: 'Restart the dashboard now?',
62
+ default: true,
63
+ }]);
64
+
65
+ if (restart) {
66
+ const port = saved.port || loadConfig().dashboardPort;
67
+ const sp = ora(`Starting dashboard on port ${port}...`).start();
68
+ const res = await startDashboard(port);
69
+ res.success
70
+ ? sp.succeed(`Dashboard restarted on port ${port}`)
71
+ : sp.fail('Could not restart dashboard — use the Dashboard menu');
72
+ }
73
+
74
+ dbSet('update-pre-dashboard', null);
75
+ }
76
+
77
+ // ─── Perform update ───────────────────────────────────────────────────────────
78
+
79
+ async function performUpdate(latestVersion) {
80
+ // Step 1 — snapshot dashboard state and persist it
81
+ const status = await getDashboardStatus();
82
+ dbSet('update-pre-dashboard', {
83
+ wasRunning: status.running,
84
+ pid: status.pid,
85
+ port: status.port,
86
+ });
87
+
88
+ // Step 2 — stop dashboard if running
89
+ if (status.running) {
90
+ const sp = ora('Stopping dashboard...').start();
91
+ await stopDashboard(status.pid);
92
+ sp.succeed('Dashboard stopped');
93
+ }
94
+
95
+ // Step 3 — install new version
96
+ const sp = ora(`Installing easy-devops@${latestVersion}...`).start();
97
+ const result = await run(`npm install -g easy-devops@${latestVersion}`, { timeout: 120000 });
98
+
99
+ if (!result.success) {
100
+ sp.fail('Update failed');
101
+ console.log(chalk.red('\n' + (result.stderr || result.stdout) + '\n'));
102
+ // Leave 'update-pre-dashboard' in DB so recovery runs on next launch
103
+ return false;
104
+ }
105
+
106
+ sp.succeed(`Updated to v${latestVersion}`);
107
+
108
+ // Step 4 — restart dashboard if it was running before
109
+ const saved = dbGet('update-pre-dashboard');
110
+ dbSet('update-pre-dashboard', null);
111
+
112
+ if (saved?.wasRunning) {
113
+ const port = saved.port || loadConfig().dashboardPort;
114
+ const restSp = ora(`Restarting dashboard on port ${port}...`).start();
115
+ const res = await startDashboard(port);
116
+ res.success
117
+ ? restSp.succeed(`Dashboard restarted on port ${port}`)
118
+ : restSp.fail('Could not restart dashboard — use the Dashboard menu');
119
+ }
120
+
121
+ return true;
122
+ }
123
+
124
+ // ─── Menu ─────────────────────────────────────────────────────────────────────
125
+
126
+ export default async function updateMenu() {
127
+ // Recover from a crashed previous update first
128
+ await recoverIfNeeded();
129
+
130
+ const spinner = ora('Checking for updates...').start();
131
+ const latestVersion = await fetchLatestVersion();
132
+ spinner.stop();
133
+
134
+ console.log(chalk.bold('\n Check for Updates'));
135
+ console.log(chalk.gray(' ' + '─'.repeat(40)));
136
+ console.log(` Current version : ${chalk.cyan('v' + currentVersion)}`);
137
+
138
+ if (!latestVersion) {
139
+ console.log(chalk.yellow(' Could not reach npm registry. Check your internet connection.\n'));
140
+ await inquirer.prompt([{ type: 'input', name: '_', message: 'Press Enter to go back...' }]);
141
+ return;
142
+ }
143
+
144
+ const updateAvailable = isNewer(latestVersion, currentVersion);
145
+
146
+ if (updateAvailable) {
147
+ console.log(` Latest version : ${chalk.green('v' + latestVersion)} ${chalk.yellow('← update available')}\n`);
148
+ } else {
149
+ console.log(` Latest version : ${chalk.green('v' + latestVersion)} ${chalk.gray('✓ up to date')}\n`);
150
+ }
151
+
152
+ const choices = updateAvailable
153
+ ? [`Update to v${latestVersion}`, new inquirer.Separator(), '← Back']
154
+ : ['← Back'];
155
+
156
+ let choice;
157
+ try {
158
+ ({ choice } = await inquirer.prompt([{
159
+ type: 'list',
160
+ name: 'choice',
161
+ message: 'Select an option:',
162
+ choices,
163
+ }]));
164
+ } catch (err) {
165
+ if (err.name === 'ExitPromptError') return;
166
+ throw err;
167
+ }
168
+
169
+ if (choice === `Update to v${latestVersion}`) {
170
+ const success = await performUpdate(latestVersion);
171
+ if (success) {
172
+ console.log(chalk.gray('\n Restart easy-devops to use the new version.\n'));
173
+ }
174
+ try {
175
+ await inquirer.prompt([{ type: 'input', name: '_', message: 'Press Enter to continue...' }]);
176
+ } catch { /* ExitPromptError */ }
177
+ }
178
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easy-devops",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "A unified DevOps management tool with CLI and web dashboard for managing Nginx, SSL certificates, and Node.js on Linux and Windows servers.",
5
5
  "keywords": [
6
6
  "devops",