easy-devops 0.1.3 → 0.1.5

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.
@@ -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`));
@@ -247,37 +247,63 @@ async function installCertbot() {
247
247
  return { success: exitCode === 0 };
248
248
  }
249
249
 
250
- // ── Windows: winget → Chocolatey → direct EFF installer ──────────────────────
250
+ // ── Shared helpers ────────────────────────────────────────────────────────────
251
+
252
+ // Check multiple possible certbot locations — some methods install to different paths
253
+ async function verifyCertbot() {
254
+ const whereResult = await run('where.exe certbot 2>$null');
255
+ if (whereResult.success && whereResult.stdout.trim()) return true;
256
+ const paths = [
257
+ CERTBOT_WIN_EXE,
258
+ 'C:\\Program Files (x86)\\Certbot\\bin\\certbot.exe',
259
+ 'C:\\Certbot\\bin\\certbot.exe',
260
+ ];
261
+ for (const p of paths) {
262
+ const r = await run(`Test-Path '${p}'`);
263
+ if (r.stdout.trim().toLowerCase() === 'true') return true;
264
+ }
265
+ return false;
266
+ }
251
267
 
252
- // 1. Try winget (Windows 10/11 desktop; not on Windows Server by default)
268
+ // Download a file trying Invoke-WebRequest (TLS 1.2 forced) then curl.exe
269
+ const hasCurl = (await run('where.exe curl.exe 2>$null')).success;
270
+ async function downloadFile(url, dest) {
271
+ const iwr = await run(
272
+ `[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '${url}' -OutFile '${dest}' -UseBasicParsing -TimeoutSec 120`,
273
+ { timeout: 130000 },
274
+ );
275
+ if (iwr.success) return true;
276
+ if (hasCurl) {
277
+ const curl = await run(`curl.exe -L --silent --show-error --max-time 120 -o '${dest}' '${url}'`, { timeout: 130000 });
278
+ if (curl.success) return true;
279
+ }
280
+ return false;
281
+ }
282
+
283
+ // ── Method 1: winget ──────────────────────────────────────────────────────────
253
284
  const wingetCheck = await run('where.exe winget 2>$null');
254
- if (wingetCheck.success && wingetCheck.stdout.trim().length > 0) {
255
- console.log(chalk.gray('\n Installing via winget (EFF.Certbot) ...\n'));
285
+ if (wingetCheck.success && wingetCheck.stdout.trim()) {
286
+ console.log(chalk.gray('\n [1/4] Trying winget ...\n'));
256
287
  const exitCode = await runLive(
257
288
  'winget install -e --id EFF.Certbot --accept-package-agreements --accept-source-agreements',
258
289
  { timeout: 180000 },
259
290
  );
260
- if (exitCode === 0) {
261
- const check = await run(`Test-Path "${CERTBOT_WIN_EXE}"`);
262
- return { success: check.stdout.trim().toLowerCase() === 'true' };
263
- }
264
- console.log(chalk.yellow(' winget failed, trying Chocolatey...\n'));
291
+ if (exitCode === 0 || await verifyCertbot()) return { success: true };
292
+ console.log(chalk.yellow(' winget did not install certbot, trying next method...\n'));
265
293
  }
266
294
 
267
- // 2. Try Chocolatey (common on Windows Server)
295
+ // ── Method 2: Chocolatey ──────────────────────────────────────────────────────
268
296
  const chocoCheck = await run('where.exe choco 2>$null');
269
- if (chocoCheck.success && chocoCheck.stdout.trim().length > 0) {
270
- console.log(chalk.gray('\n Installing via Chocolatey ...\n'));
297
+ if (chocoCheck.success && chocoCheck.stdout.trim()) {
298
+ console.log(chalk.gray('\n [2/4] Trying Chocolatey ...\n'));
271
299
  const exitCode = await runLive('choco install certbot -y', { timeout: 180000 });
272
- if (exitCode === 0) {
273
- const check = await run(`Test-Path "${CERTBOT_WIN_EXE}"`);
274
- return { success: check.stdout.trim().toLowerCase() === 'true' };
275
- }
276
- console.log(chalk.yellow(' Chocolatey failed, trying direct download...\n'));
300
+ if (exitCode === 0 || await verifyCertbot()) return { success: true };
301
+ console.log(chalk.yellow(' Chocolatey did not install certbot, trying next method...\n'));
277
302
  }
278
303
 
279
- // 3. Direct download try multiple sources in order
304
+ // ── Method 3: Official NSIS installer (GitHub dl.eff.org) ──────────────────
280
305
  const INSTALLER_FILENAME = 'certbot-beta-installer-win_amd64_signed.exe';
306
+ const INSTALLER_DEST = `$env:TEMP\\${INSTALLER_FILENAME}`;
281
307
  const downloadSources = [
282
308
  `https://github.com/certbot/certbot/releases/latest/download/${INSTALLER_FILENAME}`,
283
309
  `https://dl.eff.org/${INSTALLER_FILENAME}`,
@@ -286,40 +312,44 @@ async function installCertbot() {
286
312
  let downloaded = false;
287
313
  for (const url of downloadSources) {
288
314
  const label = new URL(url).hostname;
289
- console.log(chalk.gray(`\n Downloading certbot installer from ${label} ...\n`));
315
+ console.log(chalk.gray(`\n [3/4] Downloading certbot installer from ${label} ...\n`));
316
+ if (await downloadFile(url, INSTALLER_DEST)) { downloaded = true; break; }
317
+ console.log(chalk.yellow(` Failed from ${label}`));
318
+ }
290
319
 
291
- const downloadResult = await run(
292
- `$ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '${url}' -OutFile "$env:TEMP\\certbot-installer.exe" -UseBasicParsing -TimeoutSec 120`,
293
- { timeout: 130000 },
320
+ if (downloaded) {
321
+ console.log(chalk.gray(' Running installer silently ...\n'));
322
+ // Use Start-Process -PassThru -Wait so PowerShell waits for the NSIS process to fully exit
323
+ await run(
324
+ `$p = Start-Process -FilePath "${INSTALLER_DEST}" -ArgumentList '/S' -PassThru -Wait; $p.ExitCode`,
325
+ { timeout: 120000 },
294
326
  );
295
-
296
- if (downloadResult.success) {
297
- downloaded = true;
298
- break;
299
- }
300
-
301
- console.log(chalk.yellow(` Failed from ${label}: ${(downloadResult.stderr || downloadResult.stdout).split('\n')[0].trim()}`));
327
+ await run(`Remove-Item -Force "${INSTALLER_DEST}" -ErrorAction SilentlyContinue`);
328
+ // Allow a few seconds for the installer to finish writing files
329
+ await new Promise(r => setTimeout(r, 4000));
330
+ if (await verifyCertbot()) return { success: true };
331
+ console.log(chalk.yellow(' Installer ran but certbot not found, trying pip...\n'));
302
332
  }
303
333
 
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'));
307
- return { success: false };
334
+ // ── Method 4: pip (Python must be installed) ──────────────────────────────────
335
+ const pipCheck = await run('where.exe pip 2>$null');
336
+ if (pipCheck.success && pipCheck.stdout.trim()) {
337
+ console.log(chalk.gray('\n [4/4] Trying pip install certbot ...\n'));
338
+ const exitCode = await runLive('pip install certbot', { timeout: 180000 });
339
+ if (exitCode === 0 || await verifyCertbot()) return { success: true };
340
+ } else {
341
+ // try pip3 as well
342
+ const pip3Check = await run('where.exe pip3 2>$null');
343
+ if (pip3Check.success && pip3Check.stdout.trim()) {
344
+ console.log(chalk.gray('\n [4/4] Trying pip3 install certbot ...\n'));
345
+ const exitCode = await runLive('pip3 install certbot', { timeout: 180000 });
346
+ if (exitCode === 0 || await verifyCertbot()) return { success: true };
347
+ }
308
348
  }
309
349
 
310
- console.log(chalk.gray(' Running installer silently ...\n'));
311
-
312
- await run(
313
- `Start-Process -FilePath "$env:TEMP\\certbot-installer.exe" -ArgumentList '/S' -Wait -NoNewWindow`,
314
- { timeout: 120000 },
315
- );
316
-
317
- // Cleanup installer
318
- await run(`Remove-Item -Force "$env:TEMP\\certbot-installer.exe" -ErrorAction SilentlyContinue`);
319
-
320
- // Verify
321
- const check = await run(`Test-Path "${CERTBOT_WIN_EXE}"`);
322
- return { success: check.stdout.trim().toLowerCase() === 'true' };
350
+ // All methods exhausted
351
+ console.log(chalk.gray('\n Manual install: https://certbot.eff.org/instructions?ws=other&os=windows\n'));
352
+ return { success: false };
323
353
  }
324
354
 
325
355
  // ─── showSslManager ───────────────────────────────────────────────────────────
@@ -21,7 +21,7 @@ import path from 'path';
21
21
  import { fileURLToPath } from 'url';
22
22
  import { createRequire } from 'module';
23
23
  import { run } from '../../core/shell.js';
24
- import { dbGet, dbSet } from '../../core/db.js';
24
+ import { dbGet, dbSet, closeDb } from '../../core/db.js';
25
25
  import { loadConfig } from '../../core/config.js';
26
26
  import { getDashboardStatus, startDashboard, stopDashboard } from './dashboard.js';
27
27
 
@@ -92,7 +92,10 @@ async function performUpdate(latestVersion) {
92
92
  sp.succeed('Dashboard stopped');
93
93
  }
94
94
 
95
- // Step 3 — install new version
95
+ // Step 3 — close the SQLite connection so npm can rename the db file (EBUSY on Windows)
96
+ closeDb();
97
+
98
+ // Step 4 — install new version
96
99
  const sp = ora(`Installing easy-devops@${latestVersion}...`).start();
97
100
  const result = await run(`npm install -g easy-devops@${latestVersion}`, { timeout: 120000 });
98
101
 
@@ -105,7 +108,7 @@ async function performUpdate(latestVersion) {
105
108
 
106
109
  sp.succeed(`Updated to v${latestVersion}`);
107
110
 
108
- // Step 4 — restart dashboard if it was running before
111
+ // Step 5 — restart dashboard if it was running before
109
112
  const saved = dbGet('update-pre-dashboard');
110
113
  dbSet('update-pre-dashboard', null);
111
114
 
package/core/db.js CHANGED
@@ -28,3 +28,14 @@ export { db };
28
28
  export const dbGet = (key) => db.get(key);
29
29
  export const dbSet = (key, value) => db.set(key, value);
30
30
  export const dbDelete = (key) => db.delete(key);
31
+
32
+ /**
33
+ * Closes the underlying SQLite connection.
34
+ * Call this before any operation that needs to rename or replace the db file
35
+ * (e.g. npm install -g), otherwise the open file handle causes EBUSY on Windows.
36
+ */
37
+ export function closeDb() {
38
+ try {
39
+ db.driver?.db?.close?.();
40
+ } catch { /* ignore */ }
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easy-devops",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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",