easy-devops 0.1.8 → 0.2.0

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.
@@ -310,51 +310,119 @@ async function installCertbot() {
310
310
  }
311
311
 
312
312
  const hasCurl = (await run('where.exe curl.exe 2>$null')).success;
313
+ let lastDownloadError = '';
313
314
 
314
- async function downloadFile(url, dest) {
315
+ async function downloadFile(url, dest, showErrors = false) {
315
316
  const safeUrl = url.replace(/'/g, "''");
316
317
  const safeDest = dest.replace(/'/g, "''");
317
318
 
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`,
319
+ // Enable all TLS versions (1.0, 1.1, 1.2, 1.3) for maximum compatibility
320
+ const tlsSetup = `[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13`;
321
+
322
+ // Helper to safely run a command and catch EPERM errors
323
+ const safeRun = async (cmd, opts) => {
324
+ try {
325
+ return await run(cmd, opts);
326
+ } catch (err) {
327
+ // Re-throw EPERM so caller can handle Windows Defender blocks
328
+ if (err.code === 'EPERM' || err.message?.includes('EPERM')) {
329
+ throw err;
330
+ }
331
+ return { success: false, stdout: '', stderr: err.message, exitCode: null };
332
+ }
333
+ };
334
+
335
+ // Method 1: Invoke-WebRequest with all TLS versions
336
+ let r = await safeRun(
337
+ `${tlsSetup}; $ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '${safeUrl}' -OutFile '${safeDest}' -UseBasicParsing -TimeoutSec 120`,
321
338
  { timeout: 130000 },
322
339
  );
323
340
  if (r.success) return true;
341
+ if (showErrors && r.stderr) lastDownloadError = `Invoke-WebRequest: ${r.stderr}`;
324
342
 
325
- // Method 2: WebClient
326
- r = await run(
327
- `[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (New-Object System.Net.WebClient).DownloadFile('${safeUrl}','${safeDest}')`,
343
+ // Method 2: WebClient with TLS
344
+ r = await safeRun(
345
+ `${tlsSetup}; (New-Object System.Net.WebClient).DownloadFile('${safeUrl}','${safeDest}')`,
328
346
  { timeout: 130000 },
329
347
  );
330
348
  if (r.success) return true;
349
+ if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `WebClient: ${r.stderr}`;
331
350
 
332
- // Method 3: BITS
333
- r = await run(
351
+ // Method 3: BITS Transfer
352
+ r = await safeRun(
334
353
  `Import-Module BitsTransfer -ErrorAction SilentlyContinue; Start-BitsTransfer -Source '${safeUrl}' -Destination '${safeDest}' -ErrorAction Stop`,
335
354
  { timeout: 130000 },
336
355
  );
337
356
  if (r.success) return true;
357
+ if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `BITS: ${r.stderr}`;
338
358
 
339
- // Method 4: curl.exe
359
+ // Method 4: curl.exe (works on Windows 10+)
340
360
  if (hasCurl) {
341
- r = await run(
342
- `curl.exe -L --ssl-no-revoke --silent --show-error --max-time 120 -o '${safeDest}' '${safeUrl}'`,
361
+ r = await safeRun(
362
+ `curl.exe -L --ssl-no-revoke --max-time 120 -o '${safeDest}' '${safeUrl}'`,
343
363
  { timeout: 130000 },
344
364
  );
345
365
  if (r.success) return true;
366
+ if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `curl: ${r.stderr}`;
346
367
  }
347
368
 
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)`,
369
+ // Method 5: HttpClient with custom handler
370
+ r = await safeRun(
371
+ `${tlsSetup}; $handler=[System.Net.Http.HttpClientHandler]::new(); $handler.ServerCertificateCustomValidationCallback={$true}; $handler.AllowAutoRedirect=$true; $hc=[System.Net.Http.HttpClient]::new($handler); $hc.DefaultRequestHeaders.Add('User-Agent','Mozilla/5.0 (Windows NT 10.0; Win64; x64)'); $hc.Timeout=[TimeSpan]::FromSeconds(120); $bytes=$hc.GetByteArrayAsync('${safeUrl}').GetAwaiter().GetResult(); [System.IO.File]::WriteAllBytes('${safeDest}',$bytes)`,
372
+ { timeout: 130000 },
373
+ );
374
+ if (r.success) return true;
375
+ if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `HttpClient: ${r.stderr}`;
376
+
377
+ // Method 6: Certutil (built-in Windows tool)
378
+ r = await safeRun(
379
+ `certutil.exe -urlcache -split -f "${safeUrl}" "${safeDest}"`,
380
+ { timeout: 130000 },
381
+ );
382
+ if (r.success) return true;
383
+ if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `certutil: ${r.stderr}`;
384
+
385
+ // Method 7: PowerShell with DisableKeepAlive
386
+ r = await safeRun(
387
+ `${tlsSetup}; $req=[System.Net.WebRequest]::Create('${safeUrl}'); $req.Method='GET'; $req.KeepAlive=$false; $req.UserAgent='Mozilla/5.0'; $resp=$req.GetResponse(); $stream=$resp.GetResponseStream(); $reader=[System.IO.BinaryReader]::new($stream); $bytes=$reader.ReadBytes($resp.ContentLength); $reader.Close(); [System.IO.File]::WriteAllBytes('${safeDest}',$bytes)`,
351
388
  { timeout: 130000 },
352
389
  );
353
390
  if (r.success) return true;
391
+ if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `WebRequest: ${r.stderr}`;
354
392
 
355
393
  return false;
356
394
  }
357
395
 
396
+ // Test network connectivity to common endpoints
397
+ async function testNetworkConnectivity() {
398
+ console.log(chalk.cyan('\n Testing network connectivity...'));
399
+ const tests = [
400
+ { name: 'GitHub', url: 'https://github.com' },
401
+ { name: 'Microsoft', url: 'https://microsoft.com' },
402
+ { name: 'EFF', url: 'https://eff.org' },
403
+ ];
404
+ const results = [];
405
+
406
+ for (const test of tests) {
407
+ const r = await run(`curl.exe -I --ssl-no-revoke --max-time 10 "${test.url}"`, { timeout: 15000 });
408
+ const success = r.success || r.stdout.includes('HTTP') || r.stdout.includes('200');
409
+ results.push({ name: test.name, success });
410
+ console.log(chalk.gray(` ${test.name}: ${success ? chalk.green('✓ Connected') : chalk.red('✗ Failed')}`));
411
+ }
412
+
413
+ const allFailed = results.every(r => !r.success);
414
+ if (allFailed) {
415
+ console.log(chalk.yellow('\n ⚠ All external connections failed.'));
416
+ console.log(chalk.gray(' This could indicate:'));
417
+ console.log(chalk.gray(' • Firewall blocking outbound connections'));
418
+ console.log(chalk.gray(' • Proxy server required'));
419
+ console.log(chalk.gray(' • DNS resolution issues'));
420
+ console.log(chalk.gray(' • Antivirus blocking downloads'));
421
+ }
422
+ console.log();
423
+ return results;
424
+ }
425
+
358
426
  async function runNsisInstaller(exePath) {
359
427
  await run(
360
428
  `$p = Start-Process -FilePath '${exePath}' -ArgumentList '/S' -PassThru -Wait; $p.ExitCode`,
@@ -424,7 +492,7 @@ async function installCertbot() {
424
492
  const appInstallerDest = '$env:TEMP\\AppInstaller.msixbundle';
425
493
 
426
494
  console.log(chalk.gray('\n Downloading App Installer...'));
427
- const downloaded = await downloadFile(appInstallerUrl, appInstallerDest);
495
+ let downloaded = false; try { downloaded = await downloadFile(appInstallerUrl, appInstallerDest); } catch (err) { if (err.code === 'EPERM' || err.message?.includes('EPERM')) { console.log(chalk.red('\n ? Windows Defender blocked the download.')); console.log(chalk.gray(' Add an exclusion in Windows Defender or download manually.')); console.log(chalk.gray(' Download URL: https://aka.ms/getwinget\n')); } else { console.log(chalk.yellow('\n Download failed: ' + err.message + '\n')); } }
428
496
 
429
497
  if (downloaded) {
430
498
  console.log(chalk.gray(' Running App Installer...'));
@@ -517,6 +585,9 @@ async function installCertbot() {
517
585
  // ── Method 7: Direct download win-acme (ZIP - smaller, no installer) ──────────
518
586
  const WINACME_DEST = 'C:\\Program Files\\win-acme';
519
587
 
588
+ // Test network before attempting downloads
589
+ try { await testNetworkConnectivity(); } catch (err) { console.log(chalk.yellow('Network test failed: ' + err.message)); }
590
+
520
591
  step('Downloading win-acme from GitHub ...');
521
592
  const winAcmeUrls = [
522
593
  'https://github.com/win-acme/win-acme/releases/latest/download/win-acme.zip',
@@ -528,7 +599,8 @@ async function installCertbot() {
528
599
  console.log(chalk.gray(` Downloading from ${hostname} ...`));
529
600
 
530
601
  const zipDest = `$env:TEMP\\win-acme.zip`;
531
- if (await downloadFile(url, zipDest)) {
602
+ lastDownloadError = '';
603
+ let dlOk = false; try { dlOk = await downloadFile(url, zipDest, true); } catch (err) { if (err.code === 'EPERM' || err.message?.includes('EPERM')) { console.log(chalk.red('Windows Defender blocked this download.')); console.log(chalk.gray('Use the manual installer option or add a Windows Defender exclusion.')); } } if (dlOk) {
532
604
  console.log(chalk.gray(' Extracting win-acme ...\n'));
533
605
  await run(`New-Item -ItemType Directory -Force -Path '${WINACME_DEST}'`);
534
606
  await run(`Expand-Archive -Path '${zipDest}' -DestinationPath '${WINACME_DEST}' -Force`);
@@ -541,6 +613,9 @@ async function installCertbot() {
541
613
  console.log(chalk.yellow(' Extraction succeeded but verification failed, trying next...\n'));
542
614
  } else {
543
615
  console.log(chalk.yellow(` Could not download from ${hostname}`));
616
+ if (lastDownloadError) {
617
+ console.log(chalk.gray(` Error: ${lastDownloadError.substring(0, 200)}\n`));
618
+ }
544
619
  }
545
620
  }
546
621
 
@@ -556,7 +631,8 @@ async function installCertbot() {
556
631
  const hostname = new URL(url).hostname;
557
632
  step(`Downloading certbot installer from ${hostname} ...`);
558
633
 
559
- if (await downloadFile(url, INSTALLER_DEST)) {
634
+ lastDownloadError = '';
635
+ let dlOk = false; try { dlOk = await downloadFile(url, INSTALLER_DEST, true); } catch (err) { if (err.code === 'EPERM' || err.message?.includes('EPERM')) { console.log(chalk.red('Windows Defender blocked this download.')); console.log(chalk.gray('Use the manual installer option or add a Windows Defender exclusion.')); } } if (dlOk) {
560
636
  console.log(chalk.gray(' Running installer silently ...\n'));
561
637
  const ok = await runNsisInstaller(INSTALLER_DEST);
562
638
  await run(`Remove-Item -Force '${INSTALLER_DEST}' -ErrorAction SilentlyContinue`);
@@ -564,6 +640,9 @@ async function installCertbot() {
564
640
  console.log(chalk.yellow(' Installer ran but certbot not detected, trying next...\n'));
565
641
  } else {
566
642
  console.log(chalk.yellow(` Could not download from ${hostname}`));
643
+ if (lastDownloadError) {
644
+ console.log(chalk.gray(` Error: ${lastDownloadError.substring(0, 200)}\n`));
645
+ }
567
646
  }
568
647
  }
569
648
 
@@ -580,10 +659,29 @@ async function installCertbot() {
580
659
  type: 'list',
581
660
  name: 'localChoice',
582
661
  message: 'What would you like to do?',
583
- choices: ['Specify local installer path', 'Cancel'],
662
+ choices: [
663
+ 'Open download page in browser',
664
+ 'Specify local installer path',
665
+ 'Cancel'
666
+ ],
584
667
  }]));
585
668
  } catch { return { success: false }; }
586
669
 
670
+ if (localChoice === 'Open download page in browser') {
671
+ console.log(chalk.cyan('\n Opening download pages in browser...'));
672
+ console.log(chalk.gray(' Download either win-acme.zip or certbot installer, then re-run this tool.\n'));
673
+
674
+ // Open win-acme releases
675
+ await run('Start-Process "https://github.com/win-acme/win-acme/releases/latest"');
676
+ // Also open EFF certbot page
677
+ await run('Start-Process "https://certbot.eff.org/instructions?ws=other&os=windows"');
678
+
679
+ console.log(chalk.green('✓ Browser opened. Download the installer, then run:'));
680
+ console.log(chalk.cyan(' easy-devops → SSL Manager → Install certbot → Specify local installer path\n'));
681
+
682
+ return { success: false };
683
+ }
684
+
587
685
  if (localChoice === 'Specify local installer path') {
588
686
  let localPath;
589
687
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easy-devops",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
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",