easy-devops 0.1.5 → 0.1.7

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.
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * cli/managers/ssl-manager.js
3
3
  *
4
- * SSL Manager — view certificate status, renew certificates, install certbot.
4
+ * SSL Manager — view certificate status, renew certificates, install certbot/win-acme.
5
5
  *
6
6
  * Exported functions:
7
- * - showSslManager() — interactive menu for managing SSL certificates
7
+ * - showSslManager() — interactive menu for managing SSL certificates
8
8
  *
9
9
  * All shell calls go through core/shell.js (run / runLive).
10
10
  * Platform differences (Windows/Linux) are handled via isWindows guards.
@@ -52,13 +52,11 @@ async function parseCertExpiry(certPath) {
52
52
  }
53
53
  }
54
54
 
55
- // ─── getCertbotExe ────────────────────────────────────────────────────────────
56
- // Returns the certbot command to use, or null if not installed.
57
- // On Windows, checks PATH first, then the well-known install location used by
58
- // the official EFF winget package (EFF.Certbot).
59
- // Always returns a PS-safe invocation string (& "..." for full paths).
55
+ // ─── ACME Client Detection ────────────────────────────────────────────────────
56
+ // Supports both certbot and win-acme on Windows
60
57
 
61
58
  const CERTBOT_WIN_EXE = 'C:\\Program Files\\Certbot\\bin\\certbot.exe';
59
+ const WINACME_EXE = 'C:\\Program Files\\win-acme\\wacs.exe';
62
60
 
63
61
  async function getCertbotExe() {
64
62
  if (!isWindows) {
@@ -66,19 +64,30 @@ async function getCertbotExe() {
66
64
  return (r.exitCode === 0 && r.stdout.trim()) ? 'certbot' : null;
67
65
  }
68
66
 
69
- // 1. On PATH?
67
+ // 1. certbot on PATH?
70
68
  const pathResult = await run('where.exe certbot');
71
69
  if (pathResult.exitCode === 0 && pathResult.stdout.trim()) {
72
70
  return 'certbot';
73
71
  }
74
72
 
75
- // 2. Well-known install location (winget / official EFF installer)
73
+ // 2. certbot well-known location
76
74
  const exeCheck = await run(`Test-Path "${CERTBOT_WIN_EXE}"`);
77
75
  if (exeCheck.stdout.trim().toLowerCase() === 'true') {
78
- // Must use & "..." in PowerShell to invoke a path with spaces
79
76
  return `& "${CERTBOT_WIN_EXE}"`;
80
77
  }
81
78
 
79
+ // 3. win-acme on PATH?
80
+ const wacsPathResult = await run('where.exe wacs');
81
+ if (wacsPathResult.exitCode === 0 && wacsPathResult.stdout.trim()) {
82
+ return 'wacs';
83
+ }
84
+
85
+ // 4. win-acme well-known location
86
+ const wacsCheck = await run(`Test-Path "${WINACME_EXE}"`);
87
+ if (wacsCheck.stdout.trim().toLowerCase() === 'true') {
88
+ return `& "${WINACME_EXE}"`;
89
+ }
90
+
82
91
  return null;
83
92
  }
84
93
 
@@ -86,6 +95,13 @@ async function isCertbotInstalled() {
86
95
  return (await getCertbotExe()) !== null;
87
96
  }
88
97
 
98
+ async function getAcmeClientType() {
99
+ const exe = await getCertbotExe();
100
+ if (!exe) return null;
101
+ if (exe.includes('wacs')) return 'winacme';
102
+ return 'certbot';
103
+ }
104
+
89
105
  // ─── isPort80Busy ─────────────────────────────────────────────────────────────
90
106
 
91
107
  async function isPort80Busy() {
@@ -165,7 +181,7 @@ function renderCertRow(cert) {
165
181
  const domainPadded = cert.domain.padEnd(35);
166
182
 
167
183
  if (cert.status === 'error') {
168
- console.log(` ${chalk.gray('❌')} ${chalk.gray(domainPadded)} ${chalk.gray('ERROR')}`);
184
+ console.log(` ${chalk.gray('❌')} ${chalk.gray(domainPadded)} ${chalk.gray('ERROR')}`);
169
185
  return;
170
186
  }
171
187
 
@@ -175,11 +191,11 @@ function renderCertRow(cert) {
175
191
  const daysStr = cert.daysLeft !== null ? `${cert.daysLeft}d` : '—';
176
192
 
177
193
  if (cert.status === 'healthy') {
178
- console.log(` ${chalk.green('✅')} ${chalk.green(domainPadded)} ${chalk.green(daysStr.padEnd(6))} ${chalk.green(`(${expiryStr})`)}`);
194
+ console.log(` ${chalk.green('✅')} ${chalk.green(domainPadded)} ${chalk.green(daysStr.padEnd(6))} ${chalk.green(`(${expiryStr})`)}`);
179
195
  } else if (cert.status === 'expiring') {
180
- console.log(` ${chalk.yellow('⚠️')} ${chalk.yellow(domainPadded)} ${chalk.yellow(daysStr.padEnd(6))} ${chalk.yellow(`(${expiryStr})`)}`);
196
+ console.log(` ${chalk.yellow('⚠️')} ${chalk.yellow(domainPadded)} ${chalk.yellow(daysStr.padEnd(6))} ${chalk.yellow(`(${expiryStr})`)}`);
181
197
  } else {
182
- console.log(` ${chalk.red('❌')} ${chalk.red(domainPadded)} ${chalk.red(daysStr.padEnd(6))} ${chalk.red(`(${expiryStr})`)}`);
198
+ console.log(` ${chalk.red('❌')} ${chalk.red(domainPadded)} ${chalk.red(daysStr.padEnd(6))} ${chalk.red(`(${expiryStr})`)}`);
183
199
  }
184
200
  }
185
201
 
@@ -188,24 +204,33 @@ function renderCertRow(cert) {
188
204
  async function renewCert(domain) {
189
205
  const certbotExe = await getCertbotExe();
190
206
  if (!certbotExe) {
191
- console.log(chalk.red('\n certbot not found — install it first\n'));
207
+ console.log(chalk.red('\n certbot/win-acme not found — install it first\n'));
192
208
  return { domain, success: false, exitCode: null };
193
209
  }
194
210
 
211
+ const clientType = await getAcmeClientType();
212
+
195
213
  await stopNginx();
196
214
 
197
215
  try {
198
216
  const portCheck = await isPort80Busy();
199
217
  if (portCheck.busy) {
200
- console.log(chalk.yellow(`\n ⚠ Port 80 is in use: ${portCheck.detail}`));
201
- console.log(chalk.yellow(' Stop that process before renewing.\n'));
218
+ console.log(chalk.yellow(`\n ⚠ Port 80 is in use: ${portCheck.detail}`));
219
+ console.log(chalk.yellow(' Stop that process before renewing.\n'));
202
220
  return { domain, success: false, exitCode: null };
203
221
  }
204
222
 
205
- const exitCode = await runLive(
206
- `${certbotExe} certonly --standalone -d "${domain}"`,
207
- { timeout: 120000 },
208
- );
223
+ let cmd;
224
+ if (clientType === 'winacme') {
225
+ // win-acme interactive mode - needs manual input
226
+ console.log(chalk.cyan('\n win-acme will open. Follow the prompts to renew the certificate.'));
227
+ console.log(chalk.cyan(' Select: N) Create new certificate → 2) Manual input → enter domain\n'));
228
+ cmd = certbotExe;
229
+ } else {
230
+ cmd = `${certbotExe} certonly --standalone -d "${domain}"`;
231
+ }
232
+
233
+ const exitCode = await runLive(cmd, { timeout: 120000 });
209
234
  return { domain, success: exitCode === 0, exitCode };
210
235
  } finally {
211
236
  await startNginx();
@@ -221,15 +246,21 @@ async function renewExpiring(certs) {
221
246
  const certbotExe = await getCertbotExe();
222
247
  if (!certbotExe) return [];
223
248
 
249
+ const clientType = await getAcmeClientType();
250
+
224
251
  await stopNginx();
225
252
 
226
253
  const results = [];
227
254
  try {
228
255
  for (const cert of expiring) {
229
- const exitCode = await runLive(
230
- `${certbotExe} certonly --standalone -d "${cert.domain}"`,
231
- { timeout: 120000 },
232
- );
256
+ let cmd;
257
+ if (clientType === 'winacme') {
258
+ console.log(chalk.cyan(`\n Renewing ${cert.domain} with win-acme - follow the prompts\n`));
259
+ cmd = certbotExe;
260
+ } else {
261
+ cmd = `${certbotExe} certonly --standalone -d "${cert.domain}"`;
262
+ }
263
+ const exitCode = await runLive(cmd, { timeout: 120000 });
233
264
  results.push({ domain: cert.domain, success: exitCode === 0, exitCode });
234
265
  }
235
266
  } finally {
@@ -249,7 +280,6 @@ async function installCertbot() {
249
280
 
250
281
  // ── Shared helpers ────────────────────────────────────────────────────────────
251
282
 
252
- // Check multiple possible certbot locations — some methods install to different paths
253
283
  async function verifyCertbot() {
254
284
  const whereResult = await run('where.exe certbot 2>$null');
255
285
  if (whereResult.success && whereResult.stdout.trim()) return true;
@@ -265,90 +295,245 @@ async function installCertbot() {
265
295
  return false;
266
296
  }
267
297
 
268
- // Download a file trying Invoke-WebRequest (TLS 1.2 forced) then curl.exe
298
+ async function verifyWinAcme() {
299
+ const paths = [
300
+ WINACME_EXE,
301
+ 'C:\\Program Files (x86)\\win-acme\\wacs.exe',
302
+ 'C:\\win-acme\\wacs.exe',
303
+ ];
304
+ for (const p of paths) {
305
+ const r = await run(`Test-Path '${p}'`);
306
+ if (r.stdout.trim().toLowerCase() === 'true') return true;
307
+ }
308
+ const whereResult = await run('where.exe wacs 2>$null');
309
+ return whereResult.success && whereResult.stdout.trim();
310
+ }
311
+
269
312
  const hasCurl = (await run('where.exe curl.exe 2>$null')).success;
313
+
270
314
  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`,
315
+ const safeUrl = url.replace(/'/g, "''");
316
+ const safeDest = dest.replace(/'/g, "''");
317
+
318
+ // Method 1: Invoke-WebRequest with TLS 1.2
319
+ let r = await run(
320
+ `[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '${safeUrl}' -OutFile '${safeDest}' -UseBasicParsing -TimeoutSec 120`,
321
+ { timeout: 130000 },
322
+ );
323
+ if (r.success) return true;
324
+
325
+ // Method 2: WebClient
326
+ r = await run(
327
+ `[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (New-Object System.Net.WebClient).DownloadFile('${safeUrl}','${safeDest}')`,
273
328
  { timeout: 130000 },
274
329
  );
275
- if (iwr.success) return true;
330
+ if (r.success) return true;
331
+
332
+ // Method 3: BITS
333
+ r = await run(
334
+ `Import-Module BitsTransfer -ErrorAction SilentlyContinue; Start-BitsTransfer -Source '${safeUrl}' -Destination '${safeDest}' -ErrorAction Stop`,
335
+ { timeout: 130000 },
336
+ );
337
+ if (r.success) return true;
338
+
339
+ // Method 4: curl.exe
276
340
  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;
341
+ r = await run(
342
+ `curl.exe -L --ssl-no-revoke --silent --show-error --max-time 120 -o '${safeDest}' '${safeUrl}'`,
343
+ { timeout: 130000 },
344
+ );
345
+ if (r.success) return true;
279
346
  }
347
+
348
+ // Method 5: HttpClient
349
+ r = await run(
350
+ `[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $hc=[System.Net.Http.HttpClient]::new(); $hc.DefaultRequestHeaders.Add('User-Agent','Mozilla/5.0'); $hc.Timeout=[TimeSpan]::FromSeconds(120); $bytes=$hc.GetByteArrayAsync('${safeUrl}').GetAwaiter().GetResult(); [System.IO.File]::WriteAllBytes('${safeDest}',$bytes)`,
351
+ { timeout: 130000 },
352
+ );
353
+ if (r.success) return true;
354
+
280
355
  return false;
281
356
  }
282
357
 
283
- // ── Method 1: winget ──────────────────────────────────────────────────────────
284
- const wingetCheck = await run('where.exe winget 2>$null');
285
- if (wingetCheck.success && wingetCheck.stdout.trim()) {
286
- console.log(chalk.gray('\n [1/4] Trying winget ...\n'));
358
+ async function runNsisInstaller(exePath) {
359
+ await run(
360
+ `$p = Start-Process -FilePath '${exePath}' -ArgumentList '/S' -PassThru -Wait; $p.ExitCode`,
361
+ { timeout: 120000 },
362
+ );
363
+ await new Promise(res => setTimeout(res, 4000));
364
+ return verifyCertbot();
365
+ }
366
+
367
+ let methodNum = 0;
368
+ function step(label) {
369
+ methodNum++;
370
+ console.log(chalk.gray(`\n [${methodNum}] ${label}\n`));
371
+ }
372
+
373
+ // ── Method 1: winget (win-acme - has better success rate) ─────────────────────
374
+ if ((await run('where.exe winget 2>$null')).success) {
375
+ step('Trying winget (win-acme) ...');
376
+ const exitCode = await runLive(
377
+ 'winget install -e --id win-acme.win-acme --accept-package-agreements --accept-source-agreements',
378
+ { timeout: 180000 },
379
+ );
380
+ if (exitCode === 0 || await verifyWinAcme()) return { success: true, client: 'winacme' };
381
+ console.log(chalk.yellow(' winget win-acme failed, trying next...\n'));
382
+ }
383
+
384
+ // ── Method 2: winget (certbot EFF) ────────────────────────────────────────────
385
+ if ((await run('where.exe winget 2>$null')).success) {
386
+ step('Trying winget (EFF.Certbot) ...');
287
387
  const exitCode = await runLive(
288
388
  'winget install -e --id EFF.Certbot --accept-package-agreements --accept-source-agreements',
289
389
  { timeout: 180000 },
290
390
  );
291
391
  if (exitCode === 0 || await verifyCertbot()) return { success: true };
292
- console.log(chalk.yellow(' winget did not install certbot, trying next method...\n'));
392
+ console.log(chalk.yellow(' winget certbot failed, trying next...\n'));
293
393
  }
294
394
 
295
- // ── Method 2: Chocolatey ──────────────────────────────────────────────────────
296
- const chocoCheck = await run('where.exe choco 2>$null');
297
- if (chocoCheck.success && chocoCheck.stdout.trim()) {
298
- console.log(chalk.gray('\n [2/4] Trying Chocolatey ...\n'));
395
+ // ── Method 3: Chocolatey (win-acme) ───────────────────────────────────────────
396
+ if ((await run('where.exe choco 2>$null')).success) {
397
+ step('Trying Chocolatey (win-acme) ...');
398
+ const exitCode = await runLive('choco install win-acme -y', { timeout: 180000 });
399
+ if (exitCode === 0 || await verifyWinAcme()) return { success: true, client: 'winacme' };
400
+ console.log(chalk.yellow(' Chocolatey win-acme failed, trying next...\n'));
401
+ }
402
+
403
+ // ── Method 4: Chocolatey (certbot) ────────────────────────────────────────────
404
+ if ((await run('where.exe choco 2>$null')).success) {
405
+ step('Trying Chocolatey (certbot) ...');
299
406
  const exitCode = await runLive('choco install certbot -y', { timeout: 180000 });
300
407
  if (exitCode === 0 || await verifyCertbot()) return { success: true };
301
- console.log(chalk.yellow(' Chocolatey did not install certbot, trying next method...\n'));
408
+ console.log(chalk.yellow(' Chocolatey certbot failed, trying next...\n'));
409
+ }
410
+
411
+ // ── Method 5: pip (certbot) ───────────────────────────────────────────────────
412
+ for (const pip of ['pip', 'pip3']) {
413
+ const check = await run(`where.exe ${pip} 2>$null`);
414
+ if (check.success && check.stdout.trim()) {
415
+ step(`Trying ${pip} install certbot ...`);
416
+ const exitCode = await runLive(`${pip} install certbot`, { timeout: 180000 });
417
+ if (exitCode === 0 || await verifyCertbot()) return { success: true };
418
+ console.log(chalk.yellow(` ${pip} did not install certbot, trying next...\n`));
419
+ break;
420
+ }
421
+ }
422
+
423
+ // ── Method 6: Scoop (win-acme) ────────────────────────────────────────────────
424
+ if ((await run('where.exe scoop 2>$null')).success) {
425
+ step('Trying Scoop (win-acme) ...');
426
+ await runLive('scoop bucket add extras', { timeout: 60000 });
427
+ const exitCode = await runLive('scoop install win-acme', { timeout: 180000 });
428
+ if (exitCode === 0 || await verifyWinAcme()) return { success: true, client: 'winacme' };
429
+ console.log(chalk.yellow(' Scoop win-acme failed, trying next...\n'));
430
+ }
431
+
432
+ // ── Method 7: Direct download win-acme (ZIP - smaller, no installer) ──────────
433
+ const WINACME_DEST = 'C:\\Program Files\\win-acme';
434
+
435
+ step('Downloading win-acme from GitHub ...');
436
+ const winAcmeUrls = [
437
+ 'https://github.com/win-acme/win-acme/releases/latest/download/win-acme.zip',
438
+ 'https://github.com/win-acme/win-acme/releases/download/v2.2.9.1/win-acme.v2.2.9.1.zip',
439
+ ];
440
+
441
+ for (const url of winAcmeUrls) {
442
+ const hostname = new URL(url).hostname;
443
+ console.log(chalk.gray(` Downloading from ${hostname} ...`));
444
+
445
+ const zipDest = `$env:TEMP\\win-acme.zip`;
446
+ if (await downloadFile(url, zipDest)) {
447
+ console.log(chalk.gray(' Extracting win-acme ...\n'));
448
+ await run(`New-Item -ItemType Directory -Force -Path '${WINACME_DEST}'`);
449
+ await run(`Expand-Archive -Path '${zipDest}' -DestinationPath '${WINACME_DEST}' -Force`);
450
+ await run(`Remove-Item -Force '${zipDest}' -ErrorAction SilentlyContinue`);
451
+
452
+ if (await verifyWinAcme()) {
453
+ console.log(chalk.green(` win-acme installed to ${WINACME_DEST}\n`));
454
+ return { success: true, client: 'winacme' };
455
+ }
456
+ console.log(chalk.yellow(' Extraction succeeded but verification failed, trying next...\n'));
457
+ } else {
458
+ console.log(chalk.yellow(` Could not download from ${hostname}`));
459
+ }
302
460
  }
303
461
 
304
- // ── Method 3: Official NSIS installer (GitHub → dl.eff.org) ──────────────────
462
+ // ── Method 8: Direct download certbot installer ────────────────────────────────
305
463
  const INSTALLER_FILENAME = 'certbot-beta-installer-win_amd64_signed.exe';
306
- const INSTALLER_DEST = `$env:TEMP\\${INSTALLER_FILENAME}`;
307
- const downloadSources = [
308
- `https://github.com/certbot/certbot/releases/latest/download/${INSTALLER_FILENAME}`,
464
+ const INSTALLER_DEST = `$env:TEMP\\${INSTALLER_FILENAME}`;
465
+ const certbotUrls = [
309
466
  `https://dl.eff.org/${INSTALLER_FILENAME}`,
467
+ `https://github.com/certbot/certbot/releases/latest/download/${INSTALLER_FILENAME}`,
310
468
  ];
311
469
 
312
- let downloaded = false;
313
- for (const url of downloadSources) {
314
- const label = new URL(url).hostname;
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
- }
470
+ for (const url of certbotUrls) {
471
+ const hostname = new URL(url).hostname;
472
+ step(`Downloading certbot installer from ${hostname} ...`);
319
473
 
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 },
326
- );
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'));
474
+ if (await downloadFile(url, INSTALLER_DEST)) {
475
+ console.log(chalk.gray(' Running installer silently ...\n'));
476
+ const ok = await runNsisInstaller(INSTALLER_DEST);
477
+ await run(`Remove-Item -Force '${INSTALLER_DEST}' -ErrorAction SilentlyContinue`);
478
+ if (ok) return { success: true };
479
+ console.log(chalk.yellow(' Installer ran but certbot not detected, trying next...\n'));
480
+ } else {
481
+ console.log(chalk.yellow(` Could not download from ${hostname}`));
482
+ }
332
483
  }
333
484
 
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 };
485
+ // ── Method 9: Manual installer path ───────────────────────────────────────────
486
+ console.log(chalk.yellow('\n All automatic methods failed.'));
487
+ console.log(chalk.gray(' You can manually download one of these on another PC:'));
488
+ console.log(chalk.gray(' • certbot: https://certbot.eff.org/instructions?ws=other&os=windows'));
489
+ console.log(chalk.gray(' win-acme: https://github.com/win-acme/win-acme/releases'));
490
+ console.log(chalk.gray(' Then transfer to this server and use "Specify local installer" below.\n'));
491
+
492
+ let localChoice;
493
+ try {
494
+ ({ localChoice } = await inquirer.prompt([{
495
+ type: 'list',
496
+ name: 'localChoice',
497
+ message: 'What would you like to do?',
498
+ choices: ['Specify local installer path', 'Cancel'],
499
+ }]));
500
+ } catch { return { success: false }; }
501
+
502
+ if (localChoice === 'Specify local installer path') {
503
+ let localPath;
504
+ try {
505
+ ({ localPath } = await inquirer.prompt([{
506
+ type: 'input',
507
+ name: 'localPath',
508
+ message: 'Full path to installer (.exe or .zip):',
509
+ validate: v => v.trim().length > 0 || 'Required',
510
+ }]));
511
+ } catch { return { success: false }; }
512
+
513
+ const exists = await run(`Test-Path '${localPath.trim()}'`);
514
+ if (exists.stdout.trim().toLowerCase() !== 'true') {
515
+ console.log(chalk.red(` File not found: ${localPath}\n`));
516
+ return { success: false };
517
+ }
518
+
519
+ const ext = path.extname(localPath.trim().toLowerCase());
520
+
521
+ if (ext === '.zip') {
522
+ step('Extracting ZIP archive ...');
523
+ await run(`New-Item -ItemType Directory -Force -Path '${WINACME_DEST}'`);
524
+ await run(`Expand-Archive -Path '${localPath.trim()}' -DestinationPath '${WINACME_DEST}' -Force`);
525
+ if (await verifyWinAcme()) {
526
+ return { success: true, client: 'winacme' };
527
+ }
528
+ console.log(chalk.red(' Extraction succeeded but win-acme not found.\n'));
529
+ } else {
530
+ step('Running installer silently ...');
531
+ const ok = await runNsisInstaller(localPath.trim());
532
+ if (ok) return { success: true };
533
+ console.log(chalk.red(' Installer ran but certbot was not detected.\n'));
347
534
  }
348
535
  }
349
536
 
350
- // All methods exhausted
351
- console.log(chalk.gray('\n Manual install: https://certbot.eff.org/instructions?ws=other&os=windows\n'));
352
537
  return { success: false };
353
538
  }
354
539
 
@@ -362,11 +547,11 @@ export async function showSslManager() {
362
547
  const certs = await listCerts(liveDir);
363
548
  spinner.stop();
364
549
 
365
- console.log(chalk.bold('\n SSL Manager'));
366
- console.log(chalk.gray(' ' + '─'.repeat(40)));
550
+ console.log(chalk.bold('\n SSL Manager'));
551
+ console.log(chalk.gray(' ' + '─'.repeat(40)));
367
552
 
368
553
  if (certs.length === 0) {
369
- console.log(chalk.gray(' No certificates found'));
554
+ console.log(chalk.gray(' No certificates found'));
370
555
  } else {
371
556
  for (const cert of certs) {
372
557
  renderCertRow(cert);
@@ -399,12 +584,12 @@ export async function showSslManager() {
399
584
  case 'Renew a certificate': {
400
585
  const installed = await isCertbotInstalled();
401
586
  if (!installed) {
402
- console.log(chalk.yellow('\n ⚠ certbot not found — select "Install certbot" first\n'));
587
+ console.log(chalk.yellow('\n ⚠ certbot/win-acme not found — select "Install certbot" first\n'));
403
588
  break;
404
589
  }
405
590
 
406
591
  if (certs.length === 0) {
407
- console.log(chalk.gray('\n No certificates found to renew\n'));
592
+ console.log(chalk.gray('\n No certificates found to renew\n'));
408
593
  break;
409
594
  }
410
595
 
@@ -423,9 +608,9 @@ export async function showSslManager() {
423
608
 
424
609
  const renewResult = await renewCert(selectedDomain);
425
610
  if (renewResult.success) {
426
- console.log(chalk.green('\n ✓ Renewed successfully\n'));
611
+ console.log(chalk.green('\n ✓ Renewed successfully\n'));
427
612
  } else {
428
- console.log(chalk.red('\n ✗ Renewal failed — see output above\n'));
613
+ console.log(chalk.red('\n ✗ Renewal failed — see output above\n'));
429
614
  }
430
615
  break;
431
616
  }
@@ -433,20 +618,20 @@ export async function showSslManager() {
433
618
  case 'Renew all expiring (< 30 days)': {
434
619
  const installed = await isCertbotInstalled();
435
620
  if (!installed) {
436
- console.log(chalk.yellow('\n ⚠ certbot not found — select "Install certbot" first\n'));
621
+ console.log(chalk.yellow('\n ⚠ certbot/win-acme not found — select "Install certbot" first\n'));
437
622
  break;
438
623
  }
439
624
 
440
625
  const results = await renewExpiring(certs);
441
626
  if (results.length === 0) {
442
- console.log(chalk.gray('\n No certificates expiring within 30 days\n'));
627
+ console.log(chalk.gray('\n No certificates expiring within 30 days\n'));
443
628
  } else {
444
629
  console.log();
445
630
  for (const r of results) {
446
631
  if (r.success) {
447
- console.log(` ${chalk.green('✓ ' + r.domain)}`);
632
+ console.log(` ${chalk.green('✓ ' + r.domain)}`);
448
633
  } else {
449
- console.log(` ${chalk.red('✗ ' + r.domain)}`);
634
+ console.log(` ${chalk.red('✗ ' + r.domain)}`);
450
635
  }
451
636
  }
452
637
  console.log();
@@ -457,15 +642,17 @@ export async function showSslManager() {
457
642
  case 'Install certbot': {
458
643
  const alreadyInstalled = await isCertbotInstalled();
459
644
  if (alreadyInstalled) {
460
- console.log(chalk.gray('\n certbot is already installed\n'));
645
+ const clientType = await getAcmeClientType();
646
+ console.log(chalk.gray(`\n ${clientType === 'winacme' ? 'win-acme' : 'certbot'} is already installed\n`));
461
647
  break;
462
648
  }
463
649
 
464
650
  const installResult = await installCertbot();
465
651
  if (installResult.success) {
466
- console.log(chalk.green('\n ✓ certbot installed successfully\n'));
652
+ const clientName = installResult.client === 'winacme' ? 'win-acme' : 'certbot';
653
+ console.log(chalk.green(`\n ✓ ${clientName} installed successfully\n`));
467
654
  } else {
468
- console.log(chalk.red('\n ✗ Installation failed\n'));
655
+ console.log(chalk.red('\n ✗ Installation failed\n'));
469
656
  }
470
657
  break;
471
658
  }
@@ -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, closeDb } from '../../core/db.js';
24
+ import { dbGet, dbSet, closeDb, initDb } from '../../core/db.js';
25
25
  import { loadConfig } from '../../core/config.js';
26
26
  import { getDashboardStatus, startDashboard, stopDashboard } from './dashboard.js';
27
27
 
@@ -108,6 +108,9 @@ async function performUpdate(latestVersion) {
108
108
 
109
109
  sp.succeed(`Updated to v${latestVersion}`);
110
110
 
111
+ // Re-initialize the database connection after npm replaced the module
112
+ initDb();
113
+
111
114
  // Step 5 — restart dashboard if it was running before
112
115
  const saved = dbGet('update-pre-dashboard');
113
116
  dbSet('update-pre-dashboard', null);
package/core/db.js CHANGED
@@ -16,8 +16,13 @@ try {
16
16
  }
17
17
 
18
18
  let db;
19
+
20
+ function createDbConnection() {
21
+ return new GoodDB(new SQLiteDriver({ path: DB_PATH }));
22
+ }
23
+
19
24
  try {
20
- db = new GoodDB(new SQLiteDriver({ path: DB_PATH }));
25
+ db = createDbConnection();
21
26
  } catch (err) {
22
27
  process.stderr.write(`[ERROR] Failed to initialize database at: ${DB_PATH}\n`);
23
28
  process.stderr.write(`Hint: Check that the current user has write access to: ${DATA_DIR}\n`);
@@ -39,3 +44,17 @@ export function closeDb() {
39
44
  db.driver?.db?.close?.();
40
45
  } catch { /* ignore */ }
41
46
  }
47
+
48
+ /**
49
+ * Reinitializes the database connection after it was closed.
50
+ * Call this after closeDb() when you need to use the database again.
51
+ */
52
+ export function initDb() {
53
+ try {
54
+ // Check if connection is still open
55
+ db.driver?.db?.prepare('SELECT 1');
56
+ } catch {
57
+ // Connection is closed, create a new one
58
+ db = createDbConnection();
59
+ }
60
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easy-devops",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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",