easy-devops 0.1.6 → 0.1.8

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,218 @@ 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;
373
+ // ── Check winget availability and offer to install if missing ─────────────────
374
+ let wingetAvailable = false;
375
+ const wingetCheck = await run('where.exe winget 2>$null');
376
+ wingetAvailable = wingetCheck.success && wingetCheck.stdout.trim();
377
+
378
+ if (!wingetAvailable) {
379
+ console.log(chalk.yellow('\n winget is not installed on this system.'));
380
+ console.log(chalk.gray(' winget (Windows Package Manager) provides the easiest installation method.'));
381
+
382
+ let installWinget;
383
+ try {
384
+ ({ installWinget } = await inquirer.prompt([{
385
+ type: 'confirm',
386
+ name: 'installWinget',
387
+ message: 'Would you like to install winget (App Installer) from Microsoft Store?',
388
+ default: true,
389
+ }]));
390
+ } catch { /* user cancelled */ }
391
+
392
+ if (installWinget) {
393
+ console.log(chalk.cyan('\n Opening Microsoft Store to install App Installer...'));
394
+ console.log(chalk.gray(' Please complete the installation, then run this command again.\n'));
395
+
396
+ // Try to open Microsoft Store
397
+ const storeOpened = await run(
398
+ 'Start-Process "ms-windows-store://pdp/?ProductId=9NBLGGH4NNS1"',
399
+ { timeout: 10000 }
400
+ ).catch(() => ({ success: false }));
401
+
402
+ if (storeOpened.success) {
403
+ console.log(chalk.green(' Microsoft Store opened successfully.'));
404
+ console.log(chalk.gray(' After installing App Installer, winget will be available.\n'));
405
+ } else {
406
+ console.log(chalk.yellow(' Could not open Microsoft Store directly.'));
407
+ console.log(chalk.gray(' Please manually install "App Installer" from:'));
408
+ console.log(chalk.cyan(' https://apps.microsoft.com/store/detail/app-installer/9NBLGGH4NNS1\n'));
409
+ }
410
+
411
+ // Optional: try direct download of App Installer
412
+ let tryDownload;
413
+ try {
414
+ ({ tryDownload } = await inquirer.prompt([{
415
+ type: 'confirm',
416
+ name: 'tryDownload',
417
+ message: 'Would you like to try downloading App Installer directly?',
418
+ default: false,
419
+ }]));
420
+ } catch { tryDownload = false; }
421
+
422
+ if (tryDownload) {
423
+ const appInstallerUrl = 'https://aka.ms/getwinget';
424
+ const appInstallerDest = '$env:TEMP\\AppInstaller.msixbundle';
425
+
426
+ console.log(chalk.gray('\n Downloading App Installer...'));
427
+ const downloaded = await downloadFile(appInstallerUrl, appInstallerDest);
428
+
429
+ if (downloaded) {
430
+ console.log(chalk.gray(' Running App Installer...'));
431
+ await run(`Start-Process -FilePath '${appInstallerDest}' -Wait`, { timeout: 300000 });
432
+
433
+ // Re-check for winget
434
+ const recheck = await run('where.exe winget 2>$null');
435
+ if (recheck.success && recheck.stdout.trim()) {
436
+ console.log(chalk.green('\n ✓ winget installed successfully!\n'));
437
+ wingetAvailable = true;
438
+ } else {
439
+ console.log(chalk.yellow('\n App Installer ran but winget is not yet available.'));
440
+ console.log(chalk.gray(' You may need to restart your terminal or sign out/in.\n'));
441
+ }
442
+ } else {
443
+ console.log(chalk.yellow('\n Could not download App Installer automatically.\n'));
444
+ }
445
+ }
446
+
447
+ // If still no winget, continue with other methods
448
+ if (!wingetAvailable) {
449
+ console.log(chalk.gray(' Continuing with alternative installation methods...\n'));
450
+ }
451
+ } else {
452
+ console.log(chalk.gray(' Skipping winget installation. Using alternative methods...\n'));
335
453
  }
336
454
  }
337
455
 
338
- // ── Method 2: winget ──────────────────────────────────────────────────────────
339
- if ((await run('where.exe winget 2>$null')).success) {
340
- step('Trying winget ...');
456
+ // ── Method 1: winget (win-acme - has better success rate) ─────────────────────
457
+ if (wingetAvailable) {
458
+ step('Trying winget (win-acme) ...');
459
+ console.log(chalk.gray(' Running: winget install win-acme.win-acme\n'));
460
+ const exitCode = await runLive(
461
+ 'winget install -e --id win-acme.win-acme --accept-package-agreements --accept-source-agreements',
462
+ { timeout: 180000 },
463
+ );
464
+ if (exitCode === 0 || await verifyWinAcme()) return { success: true, client: 'winacme' };
465
+ console.log(chalk.yellow(' winget win-acme failed, trying next...\n'));
466
+ }
467
+
468
+ // ── Method 2: winget (certbot EFF) ────────────────────────────────────────────
469
+ if (wingetAvailable) {
470
+ step('Trying winget (EFF.Certbot) ...');
471
+ console.log(chalk.gray(' Running: winget install EFF.Certbot\n'));
341
472
  const exitCode = await runLive(
342
473
  'winget install -e --id EFF.Certbot --accept-package-agreements --accept-source-agreements',
343
474
  { timeout: 180000 },
344
475
  );
345
476
  if (exitCode === 0 || await verifyCertbot()) return { success: true };
346
- console.log(chalk.yellow(' winget did not install certbot, trying next...\n'));
477
+ console.log(chalk.yellow(' winget certbot failed, trying next...\n'));
347
478
  }
348
479
 
349
- // ── Method 3: Chocolatey ──────────────────────────────────────────────────────
480
+ // ── Method 3: Chocolatey (win-acme) ───────────────────────────────────────────
350
481
  if ((await run('where.exe choco 2>$null')).success) {
351
- step('Trying Chocolatey ...');
482
+ step('Trying Chocolatey (win-acme) ...');
483
+ const exitCode = await runLive('choco install win-acme -y', { timeout: 180000 });
484
+ if (exitCode === 0 || await verifyWinAcme()) return { success: true, client: 'winacme' };
485
+ console.log(chalk.yellow(' Chocolatey win-acme failed, trying next...\n'));
486
+ }
487
+
488
+ // ── Method 4: Chocolatey (certbot) ────────────────────────────────────────────
489
+ if ((await run('where.exe choco 2>$null')).success) {
490
+ step('Trying Chocolatey (certbot) ...');
352
491
  const exitCode = await runLive('choco install certbot -y', { timeout: 180000 });
353
492
  if (exitCode === 0 || await verifyCertbot()) return { success: true };
354
- console.log(chalk.yellow(' Chocolatey did not install certbot, trying next...\n'));
493
+ console.log(chalk.yellow(' Chocolatey certbot failed, trying next...\n'));
355
494
  }
356
495
 
357
- // ── Method 4: Scoop ───────────────────────────────────────────────────────────
496
+ // ── Method 5: pip (certbot) ───────────────────────────────────────────────────
497
+ for (const pip of ['pip', 'pip3']) {
498
+ const check = await run(`where.exe ${pip} 2>$null`);
499
+ if (check.success && check.stdout.trim()) {
500
+ step(`Trying ${pip} install certbot ...`);
501
+ const exitCode = await runLive(`${pip} install certbot`, { timeout: 180000 });
502
+ if (exitCode === 0 || await verifyCertbot()) return { success: true };
503
+ console.log(chalk.yellow(` ${pip} did not install certbot, trying next...\n`));
504
+ break;
505
+ }
506
+ }
507
+
508
+ // ── Method 6: Scoop (win-acme) ────────────────────────────────────────────────
358
509
  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'));
510
+ step('Trying Scoop (win-acme) ...');
511
+ await runLive('scoop bucket add extras', { timeout: 60000 });
512
+ const exitCode = await runLive('scoop install win-acme', { timeout: 180000 });
513
+ if (exitCode === 0 || await verifyWinAcme()) return { success: true, client: 'winacme' };
514
+ console.log(chalk.yellow(' Scoop win-acme failed, trying next...\n'));
515
+ }
516
+
517
+ // ── Method 7: Direct download win-acme (ZIP - smaller, no installer) ──────────
518
+ const WINACME_DEST = 'C:\\Program Files\\win-acme';
519
+
520
+ step('Downloading win-acme from GitHub ...');
521
+ const winAcmeUrls = [
522
+ 'https://github.com/win-acme/win-acme/releases/latest/download/win-acme.zip',
523
+ 'https://github.com/win-acme/win-acme/releases/download/v2.2.9.1/win-acme.v2.2.9.1.zip',
524
+ ];
525
+
526
+ for (const url of winAcmeUrls) {
527
+ const hostname = new URL(url).hostname;
528
+ console.log(chalk.gray(` Downloading from ${hostname} ...`));
529
+
530
+ const zipDest = `$env:TEMP\\win-acme.zip`;
531
+ if (await downloadFile(url, zipDest)) {
532
+ console.log(chalk.gray(' Extracting win-acme ...\n'));
533
+ await run(`New-Item -ItemType Directory -Force -Path '${WINACME_DEST}'`);
534
+ await run(`Expand-Archive -Path '${zipDest}' -DestinationPath '${WINACME_DEST}' -Force`);
535
+ await run(`Remove-Item -Force '${zipDest}' -ErrorAction SilentlyContinue`);
536
+
537
+ if (await verifyWinAcme()) {
538
+ console.log(chalk.green(` win-acme installed to ${WINACME_DEST}\n`));
539
+ return { success: true, client: 'winacme' };
540
+ }
541
+ console.log(chalk.yellow(' Extraction succeeded but verification failed, trying next...\n'));
542
+ } else {
543
+ console.log(chalk.yellow(` Could not download from ${hostname}`));
544
+ }
363
545
  }
364
546
 
365
- // ── Method 5: Direct installer download (multiple sources × multiple methods) ─
547
+ // ── Method 8: Direct download certbot installer ────────────────────────────────
366
548
  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}`,
549
+ const INSTALLER_DEST = `$env:TEMP\\${INSTALLER_FILENAME}`;
550
+ const certbotUrls = [
370
551
  `https://dl.eff.org/${INSTALLER_FILENAME}`,
552
+ `https://github.com/certbot/certbot/releases/latest/download/${INSTALLER_FILENAME}`,
371
553
  ];
372
554
 
373
- for (const url of downloadSources) {
374
- const label = new URL(url).hostname;
375
- step(`Downloading installer from ${label} ...`);
555
+ for (const url of certbotUrls) {
556
+ const hostname = new URL(url).hostname;
557
+ step(`Downloading certbot installer from ${hostname} ...`);
558
+
376
559
  if (await downloadFile(url, INSTALLER_DEST)) {
377
- console.log(chalk.gray(' Running installer silently ...\n'));
560
+ console.log(chalk.gray(' Running installer silently ...\n'));
378
561
  const ok = await runNsisInstaller(INSTALLER_DEST);
379
562
  await run(`Remove-Item -Force '${INSTALLER_DEST}' -ErrorAction SilentlyContinue`);
380
563
  if (ok) return { success: true };
381
- console.log(chalk.yellow(' Installer ran but certbot not detected, trying next...\n'));
564
+ console.log(chalk.yellow(' Installer ran but certbot not detected, trying next...\n'));
382
565
  } else {
383
- console.log(chalk.yellow(` Could not download from ${label}`));
566
+ console.log(chalk.yellow(` Could not download from ${hostname}`));
384
567
  }
385
568
  }
386
569
 
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`));
570
+ // ── Method 9: Manual installer path ───────────────────────────────────────────
571
+ console.log(chalk.yellow('\n All automatic methods failed.'));
572
+ console.log(chalk.gray(' You can manually download one of these on another PC:'));
573
+ console.log(chalk.gray(' certbot: https://certbot.eff.org/instructions?ws=other&os=windows'));
574
+ console.log(chalk.gray(' • win-acme: https://github.com/win-acme/win-acme/releases'));
575
+ console.log(chalk.gray(' Then transfer to this server and use "Specify local installer" below.\n'));
391
576
 
392
577
  let localChoice;
393
578
  try {
394
579
  ({ localChoice } = await inquirer.prompt([{
395
- type: 'list',
396
- name: 'localChoice',
580
+ type: 'list',
581
+ name: 'localChoice',
397
582
  message: 'What would you like to do?',
398
583
  choices: ['Specify local installer path', 'Cancel'],
399
584
  }]));
@@ -403,23 +588,35 @@ async function installCertbot() {
403
588
  let localPath;
404
589
  try {
405
590
  ({ localPath } = await inquirer.prompt([{
406
- type: 'input',
407
- name: 'localPath',
408
- message: 'Full path to certbot installer (.exe):',
591
+ type: 'input',
592
+ name: 'localPath',
593
+ message: 'Full path to installer (.exe or .zip):',
409
594
  validate: v => v.trim().length > 0 || 'Required',
410
595
  }]));
411
596
  } catch { return { success: false }; }
412
597
 
413
598
  const exists = await run(`Test-Path '${localPath.trim()}'`);
414
599
  if (exists.stdout.trim().toLowerCase() !== 'true') {
415
- console.log(chalk.red(` File not found: ${localPath}\n`));
600
+ console.log(chalk.red(` File not found: ${localPath}\n`));
416
601
  return { success: false };
417
602
  }
418
603
 
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'));
604
+ const ext = path.extname(localPath.trim().toLowerCase());
605
+
606
+ if (ext === '.zip') {
607
+ step('Extracting ZIP archive ...');
608
+ await run(`New-Item -ItemType Directory -Force -Path '${WINACME_DEST}'`);
609
+ await run(`Expand-Archive -Path '${localPath.trim()}' -DestinationPath '${WINACME_DEST}' -Force`);
610
+ if (await verifyWinAcme()) {
611
+ return { success: true, client: 'winacme' };
612
+ }
613
+ console.log(chalk.red(' Extraction succeeded but win-acme not found.\n'));
614
+ } else {
615
+ step('Running installer silently ...');
616
+ const ok = await runNsisInstaller(localPath.trim());
617
+ if (ok) return { success: true };
618
+ console.log(chalk.red(' Installer ran but certbot was not detected.\n'));
619
+ }
423
620
  }
424
621
 
425
622
  return { success: false };
@@ -435,11 +632,11 @@ export async function showSslManager() {
435
632
  const certs = await listCerts(liveDir);
436
633
  spinner.stop();
437
634
 
438
- console.log(chalk.bold('\n SSL Manager'));
439
- console.log(chalk.gray(' ' + '─'.repeat(40)));
635
+ console.log(chalk.bold('\n SSL Manager'));
636
+ console.log(chalk.gray(' ' + '─'.repeat(40)));
440
637
 
441
638
  if (certs.length === 0) {
442
- console.log(chalk.gray(' No certificates found'));
639
+ console.log(chalk.gray(' No certificates found'));
443
640
  } else {
444
641
  for (const cert of certs) {
445
642
  renderCertRow(cert);
@@ -472,12 +669,12 @@ export async function showSslManager() {
472
669
  case 'Renew a certificate': {
473
670
  const installed = await isCertbotInstalled();
474
671
  if (!installed) {
475
- console.log(chalk.yellow('\n ⚠ certbot not found — select "Install certbot" first\n'));
672
+ console.log(chalk.yellow('\n ⚠ certbot/win-acme not found — select "Install certbot" first\n'));
476
673
  break;
477
674
  }
478
675
 
479
676
  if (certs.length === 0) {
480
- console.log(chalk.gray('\n No certificates found to renew\n'));
677
+ console.log(chalk.gray('\n No certificates found to renew\n'));
481
678
  break;
482
679
  }
483
680
 
@@ -496,9 +693,9 @@ export async function showSslManager() {
496
693
 
497
694
  const renewResult = await renewCert(selectedDomain);
498
695
  if (renewResult.success) {
499
- console.log(chalk.green('\n ✓ Renewed successfully\n'));
696
+ console.log(chalk.green('\n ✓ Renewed successfully\n'));
500
697
  } else {
501
- console.log(chalk.red('\n ✗ Renewal failed — see output above\n'));
698
+ console.log(chalk.red('\n ✗ Renewal failed — see output above\n'));
502
699
  }
503
700
  break;
504
701
  }
@@ -506,20 +703,20 @@ export async function showSslManager() {
506
703
  case 'Renew all expiring (< 30 days)': {
507
704
  const installed = await isCertbotInstalled();
508
705
  if (!installed) {
509
- console.log(chalk.yellow('\n ⚠ certbot not found — select "Install certbot" first\n'));
706
+ console.log(chalk.yellow('\n ⚠ certbot/win-acme not found — select "Install certbot" first\n'));
510
707
  break;
511
708
  }
512
709
 
513
710
  const results = await renewExpiring(certs);
514
711
  if (results.length === 0) {
515
- console.log(chalk.gray('\n No certificates expiring within 30 days\n'));
712
+ console.log(chalk.gray('\n No certificates expiring within 30 days\n'));
516
713
  } else {
517
714
  console.log();
518
715
  for (const r of results) {
519
716
  if (r.success) {
520
- console.log(` ${chalk.green('✓ ' + r.domain)}`);
717
+ console.log(` ${chalk.green('✓ ' + r.domain)}`);
521
718
  } else {
522
- console.log(` ${chalk.red('✗ ' + r.domain)}`);
719
+ console.log(` ${chalk.red('✗ ' + r.domain)}`);
523
720
  }
524
721
  }
525
722
  console.log();
@@ -530,15 +727,17 @@ export async function showSslManager() {
530
727
  case 'Install certbot': {
531
728
  const alreadyInstalled = await isCertbotInstalled();
532
729
  if (alreadyInstalled) {
533
- console.log(chalk.gray('\n certbot is already installed\n'));
730
+ const clientType = await getAcmeClientType();
731
+ console.log(chalk.gray(`\n ${clientType === 'winacme' ? 'win-acme' : 'certbot'} is already installed\n`));
534
732
  break;
535
733
  }
536
734
 
537
735
  const installResult = await installCertbot();
538
736
  if (installResult.success) {
539
- console.log(chalk.green('\n ✓ certbot installed successfully\n'));
737
+ const clientName = installResult.client === 'winacme' ? 'win-acme' : 'certbot';
738
+ console.log(chalk.green(`\n ✓ ${clientName} installed successfully\n`));
540
739
  } else {
541
- console.log(chalk.red('\n ✗ Installation failed\n'));
740
+ console.log(chalk.red('\n ✗ Installation failed\n'));
542
741
  }
543
742
  break;
544
743
  }
@@ -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.8",
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",