easy-devops 0.1.2 → 0.1.3

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
  }
@@ -276,18 +276,34 @@ 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 in order
280
+ const INSTALLER_FILENAME = 'certbot-beta-installer-win_amd64_signed.exe';
281
+ const downloadSources = [
282
+ `https://github.com/certbot/certbot/releases/latest/download/${INSTALLER_FILENAME}`,
283
+ `https://dl.eff.org/${INSTALLER_FILENAME}`,
284
+ ];
285
+
286
+ let downloaded = false;
287
+ for (const url of downloadSources) {
288
+ const label = new URL(url).hostname;
289
+ console.log(chalk.gray(`\n Downloading certbot installer from ${label} ...\n`));
290
+
291
+ const downloadResult = await run(
292
+ `$ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '${url}' -OutFile "$env:TEMP\\certbot-installer.exe" -UseBasicParsing -TimeoutSec 120`,
293
+ { timeout: 130000 },
294
+ );
281
295
 
282
- console.log(chalk.gray('\n Downloading certbot installer from dl.eff.org ...\n'));
296
+ if (downloadResult.success) {
297
+ downloaded = true;
298
+ break;
299
+ }
283
300
 
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
- );
301
+ console.log(chalk.yellow(` Failed from ${label}: ${(downloadResult.stderr || downloadResult.stdout).split('\n')[0].trim()}`));
302
+ }
288
303
 
289
- if (!downloadResult.success) {
290
- console.log(chalk.red(' Download failed: ' + (downloadResult.stderr || downloadResult.stdout)));
304
+ if (!downloaded) {
305
+ console.log(chalk.red('\n Download failed from all sources.'));
306
+ console.log(chalk.gray(' Install manually: https://certbot.eff.org/instructions?ws=other&os=windows\n'));
291
307
  return { success: false };
292
308
  }
293
309
 
@@ -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.3",
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",