easy-devops 0.1.6 → 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 {
@@ -264,44 +295,60 @@ async function installCertbot() {
264
295
  return false;
265
296
  }
266
297
 
267
- // Try every possible download mechanism — one may work even when others fail
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
+
268
312
  const hasCurl = (await run('where.exe curl.exe 2>$null')).success;
269
313
 
270
314
  async function downloadFile(url, dest) {
271
- // 1. Invoke-WebRequest with TLS 1.2 forced
315
+ const safeUrl = url.replace(/'/g, "''");
316
+ const safeDest = dest.replace(/'/g, "''");
317
+
318
+ // Method 1: Invoke-WebRequest with TLS 1.2
272
319
  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 },
320
+ `[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '${safeUrl}' -OutFile '${safeDest}' -UseBasicParsing -TimeoutSec 120`,
321
+ { timeout: 130000 },
275
322
  );
276
323
  if (r.success) return true;
277
324
 
278
- // 2. System.Net.WebClient (different .NET code path)
325
+ // Method 2: WebClient
279
326
  r = await run(
280
- `[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (New-Object System.Net.WebClient).DownloadFile('${url}','${dest}')`,
281
- { timeout: 70000 },
327
+ `[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (New-Object System.Net.WebClient).DownloadFile('${safeUrl}','${safeDest}')`,
328
+ { timeout: 130000 },
282
329
  );
283
330
  if (r.success) return true;
284
331
 
285
- // 3. BITS (Background Intelligent Transfer Service)
332
+ // Method 3: BITS
286
333
  r = await run(
287
- `Start-BitsTransfer -Source '${url}' -Destination '${dest}' -ErrorAction Stop`,
288
- { timeout: 70000 },
334
+ `Import-Module BitsTransfer -ErrorAction SilentlyContinue; Start-BitsTransfer -Source '${safeUrl}' -Destination '${safeDest}' -ErrorAction Stop`,
335
+ { timeout: 130000 },
289
336
  );
290
337
  if (r.success) return true;
291
338
 
292
- // 4. curl.exe (independent TLS stack)
339
+ // Method 4: curl.exe
293
340
  if (hasCurl) {
294
341
  r = await run(
295
- `curl.exe -L --ssl-no-revoke --silent --show-error --max-time 60 -o '${dest}' '${url}'`,
296
- { timeout: 70000 },
342
+ `curl.exe -L --ssl-no-revoke --silent --show-error --max-time 120 -o '${safeDest}' '${safeUrl}'`,
343
+ { timeout: 130000 },
297
344
  );
298
345
  if (r.success) return true;
299
346
  }
300
347
 
301
- // 5. System.Net.Http.HttpClient (most modern .NET stack)
348
+ // Method 5: HttpClient
302
349
  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 },
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 },
305
352
  );
306
353
  if (r.success) return true;
307
354
 
@@ -320,80 +367,133 @@ async function installCertbot() {
320
367
  let methodNum = 0;
321
368
  function step(label) {
322
369
  methodNum++;
323
- console.log(chalk.gray(`\n [${methodNum}] ${label}\n`));
370
+ console.log(chalk.gray(`\n [${methodNum}] ${label}\n`));
324
371
  }
325
372
 
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
- }
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'));
336
382
  }
337
383
 
338
- // ── Method 2: winget ──────────────────────────────────────────────────────────
384
+ // ── Method 2: winget (certbot EFF) ────────────────────────────────────────────
339
385
  if ((await run('where.exe winget 2>$null')).success) {
340
- step('Trying winget ...');
386
+ step('Trying winget (EFF.Certbot) ...');
341
387
  const exitCode = await runLive(
342
388
  'winget install -e --id EFF.Certbot --accept-package-agreements --accept-source-agreements',
343
389
  { timeout: 180000 },
344
390
  );
345
391
  if (exitCode === 0 || await verifyCertbot()) return { success: true };
346
- console.log(chalk.yellow(' winget did not install certbot, trying next...\n'));
392
+ console.log(chalk.yellow(' winget certbot failed, trying next...\n'));
393
+ }
394
+
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'));
347
401
  }
348
402
 
349
- // ── Method 3: Chocolatey ──────────────────────────────────────────────────────
403
+ // ── Method 4: Chocolatey (certbot) ────────────────────────────────────────────
350
404
  if ((await run('where.exe choco 2>$null')).success) {
351
- step('Trying Chocolatey ...');
405
+ step('Trying Chocolatey (certbot) ...');
352
406
  const exitCode = await runLive('choco install certbot -y', { timeout: 180000 });
353
407
  if (exitCode === 0 || await verifyCertbot()) return { success: true };
354
- console.log(chalk.yellow(' Chocolatey did not install certbot, trying next...\n'));
408
+ console.log(chalk.yellow(' Chocolatey certbot failed, trying next...\n'));
355
409
  }
356
410
 
357
- // ── Method 4: Scoop ───────────────────────────────────────────────────────────
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) ────────────────────────────────────────────────
358
424
  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'));
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
+ }
363
460
  }
364
461
 
365
- // ── Method 5: Direct installer download (multiple sources × multiple methods) ─
462
+ // ── Method 8: Direct download certbot installer ────────────────────────────────
366
463
  const INSTALLER_FILENAME = 'certbot-beta-installer-win_amd64_signed.exe';
367
- const INSTALLER_DEST = `$env:TEMP\\${INSTALLER_FILENAME}`;
368
- const downloadSources = [
369
- `https://github.com/certbot/certbot/releases/latest/download/${INSTALLER_FILENAME}`,
464
+ const INSTALLER_DEST = `$env:TEMP\\${INSTALLER_FILENAME}`;
465
+ const certbotUrls = [
370
466
  `https://dl.eff.org/${INSTALLER_FILENAME}`,
467
+ `https://github.com/certbot/certbot/releases/latest/download/${INSTALLER_FILENAME}`,
371
468
  ];
372
469
 
373
- for (const url of downloadSources) {
374
- const label = new URL(url).hostname;
375
- step(`Downloading installer from ${label} ...`);
470
+ for (const url of certbotUrls) {
471
+ const hostname = new URL(url).hostname;
472
+ step(`Downloading certbot installer from ${hostname} ...`);
473
+
376
474
  if (await downloadFile(url, INSTALLER_DEST)) {
377
- console.log(chalk.gray(' Running installer silently ...\n'));
475
+ console.log(chalk.gray(' Running installer silently ...\n'));
378
476
  const ok = await runNsisInstaller(INSTALLER_DEST);
379
477
  await run(`Remove-Item -Force '${INSTALLER_DEST}' -ErrorAction SilentlyContinue`);
380
478
  if (ok) return { success: true };
381
- console.log(chalk.yellow(' Installer ran but certbot not detected, trying next...\n'));
479
+ console.log(chalk.yellow(' Installer ran but certbot not detected, trying next...\n'));
382
480
  } else {
383
- console.log(chalk.yellow(` Could not download from ${label}`));
481
+ console.log(chalk.yellow(` Could not download from ${hostname}`));
384
482
  }
385
483
  }
386
484
 
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`));
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'));
391
491
 
392
492
  let localChoice;
393
493
  try {
394
494
  ({ localChoice } = await inquirer.prompt([{
395
- type: 'list',
396
- name: 'localChoice',
495
+ type: 'list',
496
+ name: 'localChoice',
397
497
  message: 'What would you like to do?',
398
498
  choices: ['Specify local installer path', 'Cancel'],
399
499
  }]));
@@ -403,23 +503,35 @@ async function installCertbot() {
403
503
  let localPath;
404
504
  try {
405
505
  ({ localPath } = await inquirer.prompt([{
406
- type: 'input',
407
- name: 'localPath',
408
- message: 'Full path to certbot installer (.exe):',
506
+ type: 'input',
507
+ name: 'localPath',
508
+ message: 'Full path to installer (.exe or .zip):',
409
509
  validate: v => v.trim().length > 0 || 'Required',
410
510
  }]));
411
511
  } catch { return { success: false }; }
412
512
 
413
513
  const exists = await run(`Test-Path '${localPath.trim()}'`);
414
514
  if (exists.stdout.trim().toLowerCase() !== 'true') {
415
- console.log(chalk.red(` File not found: ${localPath}\n`));
515
+ console.log(chalk.red(` File not found: ${localPath}\n`));
416
516
  return { success: false };
417
517
  }
418
518
 
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'));
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'));
534
+ }
423
535
  }
424
536
 
425
537
  return { success: false };
@@ -435,11 +547,11 @@ export async function showSslManager() {
435
547
  const certs = await listCerts(liveDir);
436
548
  spinner.stop();
437
549
 
438
- console.log(chalk.bold('\n SSL Manager'));
439
- console.log(chalk.gray(' ' + '─'.repeat(40)));
550
+ console.log(chalk.bold('\n SSL Manager'));
551
+ console.log(chalk.gray(' ' + '─'.repeat(40)));
440
552
 
441
553
  if (certs.length === 0) {
442
- console.log(chalk.gray(' No certificates found'));
554
+ console.log(chalk.gray(' No certificates found'));
443
555
  } else {
444
556
  for (const cert of certs) {
445
557
  renderCertRow(cert);
@@ -472,12 +584,12 @@ export async function showSslManager() {
472
584
  case 'Renew a certificate': {
473
585
  const installed = await isCertbotInstalled();
474
586
  if (!installed) {
475
- 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'));
476
588
  break;
477
589
  }
478
590
 
479
591
  if (certs.length === 0) {
480
- console.log(chalk.gray('\n No certificates found to renew\n'));
592
+ console.log(chalk.gray('\n No certificates found to renew\n'));
481
593
  break;
482
594
  }
483
595
 
@@ -496,9 +608,9 @@ export async function showSslManager() {
496
608
 
497
609
  const renewResult = await renewCert(selectedDomain);
498
610
  if (renewResult.success) {
499
- console.log(chalk.green('\n ✓ Renewed successfully\n'));
611
+ console.log(chalk.green('\n ✓ Renewed successfully\n'));
500
612
  } else {
501
- console.log(chalk.red('\n ✗ Renewal failed — see output above\n'));
613
+ console.log(chalk.red('\n ✗ Renewal failed — see output above\n'));
502
614
  }
503
615
  break;
504
616
  }
@@ -506,20 +618,20 @@ export async function showSslManager() {
506
618
  case 'Renew all expiring (< 30 days)': {
507
619
  const installed = await isCertbotInstalled();
508
620
  if (!installed) {
509
- 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'));
510
622
  break;
511
623
  }
512
624
 
513
625
  const results = await renewExpiring(certs);
514
626
  if (results.length === 0) {
515
- 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'));
516
628
  } else {
517
629
  console.log();
518
630
  for (const r of results) {
519
631
  if (r.success) {
520
- console.log(` ${chalk.green('✓ ' + r.domain)}`);
632
+ console.log(` ${chalk.green('✓ ' + r.domain)}`);
521
633
  } else {
522
- console.log(` ${chalk.red('✗ ' + r.domain)}`);
634
+ console.log(` ${chalk.red('✗ ' + r.domain)}`);
523
635
  }
524
636
  }
525
637
  console.log();
@@ -530,15 +642,17 @@ export async function showSslManager() {
530
642
  case 'Install certbot': {
531
643
  const alreadyInstalled = await isCertbotInstalled();
532
644
  if (alreadyInstalled) {
533
- 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`));
534
647
  break;
535
648
  }
536
649
 
537
650
  const installResult = await installCertbot();
538
651
  if (installResult.success) {
539
- 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`));
540
654
  } else {
541
- console.log(chalk.red('\n ✗ Installation failed\n'));
655
+ console.log(chalk.red('\n ✗ Installation failed\n'));
542
656
  }
543
657
  break;
544
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.6",
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",