easy-devops 0.1.4 → 0.1.6

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.
@@ -247,91 +247,182 @@ 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
+ async function verifyCertbot() {
253
+ const whereResult = await run('where.exe certbot 2>$null');
254
+ if (whereResult.success && whereResult.stdout.trim()) return true;
255
+ const paths = [
256
+ CERTBOT_WIN_EXE,
257
+ 'C:\\Program Files (x86)\\Certbot\\bin\\certbot.exe',
258
+ 'C:\\Certbot\\bin\\certbot.exe',
259
+ ];
260
+ for (const p of paths) {
261
+ const r = await run(`Test-Path '${p}'`);
262
+ if (r.stdout.trim().toLowerCase() === 'true') return true;
263
+ }
264
+ return false;
265
+ }
266
+
267
+ // Try every possible download mechanism — one may work even when others fail
268
+ const hasCurl = (await run('where.exe curl.exe 2>$null')).success;
269
+
270
+ async function downloadFile(url, dest) {
271
+ // 1. Invoke-WebRequest with TLS 1.2 forced
272
+ let r = await run(
273
+ `[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '${url}' -OutFile '${dest}' -UseBasicParsing -TimeoutSec 60`,
274
+ { timeout: 70000 },
275
+ );
276
+ if (r.success) return true;
277
+
278
+ // 2. System.Net.WebClient (different .NET code path)
279
+ r = await run(
280
+ `[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (New-Object System.Net.WebClient).DownloadFile('${url}','${dest}')`,
281
+ { timeout: 70000 },
282
+ );
283
+ if (r.success) return true;
284
+
285
+ // 3. BITS (Background Intelligent Transfer Service)
286
+ r = await run(
287
+ `Start-BitsTransfer -Source '${url}' -Destination '${dest}' -ErrorAction Stop`,
288
+ { timeout: 70000 },
289
+ );
290
+ if (r.success) return true;
291
+
292
+ // 4. curl.exe (independent TLS stack)
293
+ if (hasCurl) {
294
+ r = await run(
295
+ `curl.exe -L --ssl-no-revoke --silent --show-error --max-time 60 -o '${dest}' '${url}'`,
296
+ { timeout: 70000 },
297
+ );
298
+ if (r.success) return true;
299
+ }
300
+
301
+ // 5. System.Net.Http.HttpClient (most modern .NET stack)
302
+ r = await run(
303
+ `[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $hc=[System.Net.Http.HttpClient]::new(); $hc.DefaultRequestHeaders.Add('User-Agent','Mozilla/5.0'); $bytes=$hc.GetByteArrayAsync('${url}').GetAwaiter().GetResult(); [System.IO.File]::WriteAllBytes('${dest}',$bytes)`,
304
+ { timeout: 70000 },
305
+ );
306
+ if (r.success) return true;
307
+
308
+ return false;
309
+ }
310
+
311
+ async function runNsisInstaller(exePath) {
312
+ await run(
313
+ `$p = Start-Process -FilePath '${exePath}' -ArgumentList '/S' -PassThru -Wait; $p.ExitCode`,
314
+ { timeout: 120000 },
315
+ );
316
+ await new Promise(res => setTimeout(res, 4000));
317
+ return verifyCertbot();
318
+ }
251
319
 
252
- // 1. Try winget (Windows 10/11 desktop; not on Windows Server by default)
253
- 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'));
320
+ let methodNum = 0;
321
+ function step(label) {
322
+ methodNum++;
323
+ console.log(chalk.gray(`\n [${methodNum}] ${label}\n`));
324
+ }
325
+
326
+ // ── Method 1: pip / pip3 (PyPI CDN — completely different from GitHub/EFF) ────
327
+ for (const pip of ['pip', 'pip3']) {
328
+ const check = await run(`where.exe ${pip} 2>$null`);
329
+ if (check.success && check.stdout.trim()) {
330
+ step(`Trying ${pip} install certbot ...`);
331
+ const exitCode = await runLive(`${pip} install certbot`, { timeout: 180000 });
332
+ if (exitCode === 0 || await verifyCertbot()) return { success: true };
333
+ console.log(chalk.yellow(` ${pip} did not install certbot, trying next...\n`));
334
+ break;
335
+ }
336
+ }
337
+
338
+ // ── Method 2: winget ──────────────────────────────────────────────────────────
339
+ if ((await run('where.exe winget 2>$null')).success) {
340
+ step('Trying winget ...');
256
341
  const exitCode = await runLive(
257
342
  'winget install -e --id EFF.Certbot --accept-package-agreements --accept-source-agreements',
258
343
  { timeout: 180000 },
259
344
  );
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'));
345
+ if (exitCode === 0 || await verifyCertbot()) return { success: true };
346
+ console.log(chalk.yellow(' winget did not install certbot, trying next...\n'));
265
347
  }
266
348
 
267
- // 2. Try Chocolatey (common on Windows Server)
268
- 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'));
349
+ // ── Method 3: Chocolatey ──────────────────────────────────────────────────────
350
+ if ((await run('where.exe choco 2>$null')).success) {
351
+ step('Trying Chocolatey ...');
271
352
  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'));
353
+ if (exitCode === 0 || await verifyCertbot()) return { success: true };
354
+ console.log(chalk.yellow(' Chocolatey did not install certbot, trying next...\n'));
277
355
  }
278
356
 
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.
357
+ // ── Method 4: Scoop ───────────────────────────────────────────────────────────
358
+ if ((await run('where.exe scoop 2>$null')).success) {
359
+ step('Trying Scoop ...');
360
+ const exitCode = await runLive('scoop install certbot', { timeout: 180000 });
361
+ if (exitCode === 0 || await verifyCertbot()) return { success: true };
362
+ console.log(chalk.yellow(' Scoop did not install certbot, trying next...\n'));
363
+ }
364
+
365
+ // ── Method 5: Direct installer download (multiple sources × multiple methods) ─
283
366
  const INSTALLER_FILENAME = 'certbot-beta-installer-win_amd64_signed.exe';
284
- const INSTALLER_DEST = '$env:TEMP\\certbot-installer.exe';
367
+ const INSTALLER_DEST = `$env:TEMP\\${INSTALLER_FILENAME}`;
285
368
  const downloadSources = [
286
369
  `https://github.com/certbot/certbot/releases/latest/download/${INSTALLER_FILENAME}`,
287
370
  `https://dl.eff.org/${INSTALLER_FILENAME}`,
288
371
  ];
289
372
 
290
- const hasCurl = (await run('where.exe curl.exe 2>$null')).success;
291
-
292
- let downloaded = false;
293
373
  for (const url of downloadSources) {
294
374
  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; }
303
-
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; }
375
+ step(`Downloading installer from ${label} ...`);
376
+ if (await downloadFile(url, INSTALLER_DEST)) {
377
+ console.log(chalk.gray(' Running installer silently ...\n'));
378
+ const ok = await runNsisInstaller(INSTALLER_DEST);
379
+ await run(`Remove-Item -Force '${INSTALLER_DEST}' -ErrorAction SilentlyContinue`);
380
+ if (ok) return { success: true };
381
+ console.log(chalk.yellow(' Installer ran but certbot not detected, trying next...\n'));
382
+ } else {
383
+ console.log(chalk.yellow(` Could not download from ${label}`));
311
384
  }
312
-
313
- console.log(chalk.yellow(` Failed from ${label}`));
314
385
  }
315
386
 
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'));
319
- return { success: false };
320
- }
387
+ // ── Method 6: Local file (user manually copies the installer to the server) ───
388
+ console.log(chalk.yellow('\n All automatic methods failed.'));
389
+ console.log(chalk.gray(' If you have the certbot installer on this machine, enter its path below.'));
390
+ console.log(chalk.gray(` (Download it on another PC: https://certbot.eff.org/instructions?ws=other&os=windows)\n`));
321
391
 
322
- console.log(chalk.gray(' Running installer silently ...\n'));
392
+ let localChoice;
393
+ try {
394
+ ({ localChoice } = await inquirer.prompt([{
395
+ type: 'list',
396
+ name: 'localChoice',
397
+ message: 'What would you like to do?',
398
+ choices: ['Specify local installer path', 'Cancel'],
399
+ }]));
400
+ } catch { return { success: false }; }
401
+
402
+ if (localChoice === 'Specify local installer path') {
403
+ let localPath;
404
+ try {
405
+ ({ localPath } = await inquirer.prompt([{
406
+ type: 'input',
407
+ name: 'localPath',
408
+ message: 'Full path to certbot installer (.exe):',
409
+ validate: v => v.trim().length > 0 || 'Required',
410
+ }]));
411
+ } catch { return { success: false }; }
323
412
 
324
- await run(
325
- `Start-Process -FilePath "$env:TEMP\\certbot-installer.exe" -ArgumentList '/S' -Wait -NoNewWindow`,
326
- { timeout: 120000 },
327
- );
413
+ const exists = await run(`Test-Path '${localPath.trim()}'`);
414
+ if (exists.stdout.trim().toLowerCase() !== 'true') {
415
+ console.log(chalk.red(` File not found: ${localPath}\n`));
416
+ return { success: false };
417
+ }
328
418
 
329
- // Cleanup installer
330
- await run(`Remove-Item -Force "$env:TEMP\\certbot-installer.exe" -ErrorAction SilentlyContinue`);
419
+ step('Running local installer silently ...');
420
+ const ok = await runNsisInstaller(localPath.trim());
421
+ if (ok) return { success: true };
422
+ console.log(chalk.red(' Installer ran but certbot was not detected.\n'));
423
+ }
331
424
 
332
- // Verify
333
- const check = await run(`Test-Path "${CERTBOT_WIN_EXE}"`);
334
- return { success: check.stdout.trim().toLowerCase() === 'true' };
425
+ return { success: false };
335
426
  }
336
427
 
337
428
  // ─── 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.4",
3
+ "version": "0.1.6",
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",