easy-devops 0.1.7 → 0.1.9

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,106 @@ 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
+ // 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
+ // Method 1: Invoke-WebRequest with all TLS versions
319
323
  let r = await run(
320
- `[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '${safeUrl}' -OutFile '${safeDest}' -UseBasicParsing -TimeoutSec 120`,
324
+ `${tlsSetup}; $ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '${safeUrl}' -OutFile '${safeDest}' -UseBasicParsing -TimeoutSec 120`,
321
325
  { timeout: 130000 },
322
326
  );
323
327
  if (r.success) return true;
328
+ if (showErrors && r.stderr) lastDownloadError = `Invoke-WebRequest: ${r.stderr}`;
324
329
 
325
- // Method 2: WebClient
330
+ // Method 2: WebClient with TLS
326
331
  r = await run(
327
- `[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (New-Object System.Net.WebClient).DownloadFile('${safeUrl}','${safeDest}')`,
332
+ `${tlsSetup}; (New-Object System.Net.WebClient).DownloadFile('${safeUrl}','${safeDest}')`,
328
333
  { timeout: 130000 },
329
334
  );
330
335
  if (r.success) return true;
336
+ if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `WebClient: ${r.stderr}`;
331
337
 
332
- // Method 3: BITS
338
+ // Method 3: BITS Transfer
333
339
  r = await run(
334
340
  `Import-Module BitsTransfer -ErrorAction SilentlyContinue; Start-BitsTransfer -Source '${safeUrl}' -Destination '${safeDest}' -ErrorAction Stop`,
335
341
  { timeout: 130000 },
336
342
  );
337
343
  if (r.success) return true;
344
+ if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `BITS: ${r.stderr}`;
338
345
 
339
- // Method 4: curl.exe
346
+ // Method 4: curl.exe (works on Windows 10+)
340
347
  if (hasCurl) {
341
348
  r = await run(
342
- `curl.exe -L --ssl-no-revoke --silent --show-error --max-time 120 -o '${safeDest}' '${safeUrl}'`,
349
+ `curl.exe -L --ssl-no-revoke --max-time 120 -o '${safeDest}' '${safeUrl}'`,
343
350
  { timeout: 130000 },
344
351
  );
345
352
  if (r.success) return true;
353
+ if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `curl: ${r.stderr}`;
346
354
  }
347
355
 
348
- // Method 5: HttpClient
356
+ // Method 5: HttpClient with custom handler
357
+ r = await run(
358
+ `${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)`,
359
+ { timeout: 130000 },
360
+ );
361
+ if (r.success) return true;
362
+ if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `HttpClient: ${r.stderr}`;
363
+
364
+ // Method 6: Certutil (built-in Windows tool)
349
365
  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)`,
366
+ `certutil.exe -urlcache -split -f "${safeUrl}" "${safeDest}"`,
351
367
  { timeout: 130000 },
352
368
  );
353
369
  if (r.success) return true;
370
+ if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `certutil: ${r.stderr}`;
371
+
372
+ // Method 7: PowerShell with DisableKeepAlive
373
+ r = await run(
374
+ `${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)`,
375
+ { timeout: 130000 },
376
+ );
377
+ if (r.success) return true;
378
+ if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `WebRequest: ${r.stderr}`;
354
379
 
355
380
  return false;
356
381
  }
357
382
 
383
+ // Test network connectivity to common endpoints
384
+ async function testNetworkConnectivity() {
385
+ console.log(chalk.cyan('\n Testing network connectivity...'));
386
+ const tests = [
387
+ { name: 'GitHub', url: 'https://github.com' },
388
+ { name: 'Microsoft', url: 'https://microsoft.com' },
389
+ { name: 'EFF', url: 'https://eff.org' },
390
+ ];
391
+ const results = [];
392
+
393
+ for (const test of tests) {
394
+ const r = await run(`curl.exe -I --ssl-no-revoke --max-time 10 "${test.url}"`, { timeout: 15000 });
395
+ const success = r.success || r.stdout.includes('HTTP') || r.stdout.includes('200');
396
+ results.push({ name: test.name, success });
397
+ console.log(chalk.gray(` ${test.name}: ${success ? chalk.green('✓ Connected') : chalk.red('✗ Failed')}`));
398
+ }
399
+
400
+ const allFailed = results.every(r => !r.success);
401
+ if (allFailed) {
402
+ console.log(chalk.yellow('\n ⚠ All external connections failed.'));
403
+ console.log(chalk.gray(' This could indicate:'));
404
+ console.log(chalk.gray(' • Firewall blocking outbound connections'));
405
+ console.log(chalk.gray(' • Proxy server required'));
406
+ console.log(chalk.gray(' • DNS resolution issues'));
407
+ console.log(chalk.gray(' • Antivirus blocking downloads'));
408
+ }
409
+ console.log();
410
+ return results;
411
+ }
412
+
358
413
  async function runNsisInstaller(exePath) {
359
414
  await run(
360
415
  `$p = Start-Process -FilePath '${exePath}' -ArgumentList '/S' -PassThru -Wait; $p.ExitCode`,
@@ -370,9 +425,93 @@ async function installCertbot() {
370
425
  console.log(chalk.gray(`\n [${methodNum}] ${label}\n`));
371
426
  }
372
427
 
428
+ // ── Check winget availability and offer to install if missing ─────────────────
429
+ let wingetAvailable = false;
430
+ const wingetCheck = await run('where.exe winget 2>$null');
431
+ wingetAvailable = wingetCheck.success && wingetCheck.stdout.trim();
432
+
433
+ if (!wingetAvailable) {
434
+ console.log(chalk.yellow('\n ⚠ winget is not installed on this system.'));
435
+ console.log(chalk.gray(' winget (Windows Package Manager) provides the easiest installation method.'));
436
+
437
+ let installWinget;
438
+ try {
439
+ ({ installWinget } = await inquirer.prompt([{
440
+ type: 'confirm',
441
+ name: 'installWinget',
442
+ message: 'Would you like to install winget (App Installer) from Microsoft Store?',
443
+ default: true,
444
+ }]));
445
+ } catch { /* user cancelled */ }
446
+
447
+ if (installWinget) {
448
+ console.log(chalk.cyan('\n Opening Microsoft Store to install App Installer...'));
449
+ console.log(chalk.gray(' Please complete the installation, then run this command again.\n'));
450
+
451
+ // Try to open Microsoft Store
452
+ const storeOpened = await run(
453
+ 'Start-Process "ms-windows-store://pdp/?ProductId=9NBLGGH4NNS1"',
454
+ { timeout: 10000 }
455
+ ).catch(() => ({ success: false }));
456
+
457
+ if (storeOpened.success) {
458
+ console.log(chalk.green(' Microsoft Store opened successfully.'));
459
+ console.log(chalk.gray(' After installing App Installer, winget will be available.\n'));
460
+ } else {
461
+ console.log(chalk.yellow(' Could not open Microsoft Store directly.'));
462
+ console.log(chalk.gray(' Please manually install "App Installer" from:'));
463
+ console.log(chalk.cyan(' https://apps.microsoft.com/store/detail/app-installer/9NBLGGH4NNS1\n'));
464
+ }
465
+
466
+ // Optional: try direct download of App Installer
467
+ let tryDownload;
468
+ try {
469
+ ({ tryDownload } = await inquirer.prompt([{
470
+ type: 'confirm',
471
+ name: 'tryDownload',
472
+ message: 'Would you like to try downloading App Installer directly?',
473
+ default: false,
474
+ }]));
475
+ } catch { tryDownload = false; }
476
+
477
+ if (tryDownload) {
478
+ const appInstallerUrl = 'https://aka.ms/getwinget';
479
+ const appInstallerDest = '$env:TEMP\\AppInstaller.msixbundle';
480
+
481
+ console.log(chalk.gray('\n Downloading App Installer...'));
482
+ const downloaded = await downloadFile(appInstallerUrl, appInstallerDest);
483
+
484
+ if (downloaded) {
485
+ console.log(chalk.gray(' Running App Installer...'));
486
+ await run(`Start-Process -FilePath '${appInstallerDest}' -Wait`, { timeout: 300000 });
487
+
488
+ // Re-check for winget
489
+ const recheck = await run('where.exe winget 2>$null');
490
+ if (recheck.success && recheck.stdout.trim()) {
491
+ console.log(chalk.green('\n ✓ winget installed successfully!\n'));
492
+ wingetAvailable = true;
493
+ } else {
494
+ console.log(chalk.yellow('\n App Installer ran but winget is not yet available.'));
495
+ console.log(chalk.gray(' You may need to restart your terminal or sign out/in.\n'));
496
+ }
497
+ } else {
498
+ console.log(chalk.yellow('\n Could not download App Installer automatically.\n'));
499
+ }
500
+ }
501
+
502
+ // If still no winget, continue with other methods
503
+ if (!wingetAvailable) {
504
+ console.log(chalk.gray(' Continuing with alternative installation methods...\n'));
505
+ }
506
+ } else {
507
+ console.log(chalk.gray(' Skipping winget installation. Using alternative methods...\n'));
508
+ }
509
+ }
510
+
373
511
  // ── Method 1: winget (win-acme - has better success rate) ─────────────────────
374
- if ((await run('where.exe winget 2>$null')).success) {
512
+ if (wingetAvailable) {
375
513
  step('Trying winget (win-acme) ...');
514
+ console.log(chalk.gray(' Running: winget install win-acme.win-acme\n'));
376
515
  const exitCode = await runLive(
377
516
  'winget install -e --id win-acme.win-acme --accept-package-agreements --accept-source-agreements',
378
517
  { timeout: 180000 },
@@ -382,8 +521,9 @@ async function installCertbot() {
382
521
  }
383
522
 
384
523
  // ── Method 2: winget (certbot EFF) ────────────────────────────────────────────
385
- if ((await run('where.exe winget 2>$null')).success) {
524
+ if (wingetAvailable) {
386
525
  step('Trying winget (EFF.Certbot) ...');
526
+ console.log(chalk.gray(' Running: winget install EFF.Certbot\n'));
387
527
  const exitCode = await runLive(
388
528
  'winget install -e --id EFF.Certbot --accept-package-agreements --accept-source-agreements',
389
529
  { timeout: 180000 },
@@ -432,6 +572,9 @@ async function installCertbot() {
432
572
  // ── Method 7: Direct download win-acme (ZIP - smaller, no installer) ──────────
433
573
  const WINACME_DEST = 'C:\\Program Files\\win-acme';
434
574
 
575
+ // Test network before attempting downloads
576
+ await testNetworkConnectivity();
577
+
435
578
  step('Downloading win-acme from GitHub ...');
436
579
  const winAcmeUrls = [
437
580
  'https://github.com/win-acme/win-acme/releases/latest/download/win-acme.zip',
@@ -443,7 +586,8 @@ async function installCertbot() {
443
586
  console.log(chalk.gray(` Downloading from ${hostname} ...`));
444
587
 
445
588
  const zipDest = `$env:TEMP\\win-acme.zip`;
446
- if (await downloadFile(url, zipDest)) {
589
+ lastDownloadError = '';
590
+ if (await downloadFile(url, zipDest, true)) {
447
591
  console.log(chalk.gray(' Extracting win-acme ...\n'));
448
592
  await run(`New-Item -ItemType Directory -Force -Path '${WINACME_DEST}'`);
449
593
  await run(`Expand-Archive -Path '${zipDest}' -DestinationPath '${WINACME_DEST}' -Force`);
@@ -456,6 +600,9 @@ async function installCertbot() {
456
600
  console.log(chalk.yellow(' Extraction succeeded but verification failed, trying next...\n'));
457
601
  } else {
458
602
  console.log(chalk.yellow(` Could not download from ${hostname}`));
603
+ if (lastDownloadError) {
604
+ console.log(chalk.gray(` Error: ${lastDownloadError.substring(0, 200)}\n`));
605
+ }
459
606
  }
460
607
  }
461
608
 
@@ -471,7 +618,8 @@ async function installCertbot() {
471
618
  const hostname = new URL(url).hostname;
472
619
  step(`Downloading certbot installer from ${hostname} ...`);
473
620
 
474
- if (await downloadFile(url, INSTALLER_DEST)) {
621
+ lastDownloadError = '';
622
+ if (await downloadFile(url, INSTALLER_DEST, true)) {
475
623
  console.log(chalk.gray(' Running installer silently ...\n'));
476
624
  const ok = await runNsisInstaller(INSTALLER_DEST);
477
625
  await run(`Remove-Item -Force '${INSTALLER_DEST}' -ErrorAction SilentlyContinue`);
@@ -479,14 +627,17 @@ async function installCertbot() {
479
627
  console.log(chalk.yellow(' Installer ran but certbot not detected, trying next...\n'));
480
628
  } else {
481
629
  console.log(chalk.yellow(` Could not download from ${hostname}`));
630
+ if (lastDownloadError) {
631
+ console.log(chalk.gray(` Error: ${lastDownloadError.substring(0, 200)}\n`));
632
+ }
482
633
  }
483
634
  }
484
635
 
485
636
  // ── Method 9: Manual installer path ───────────────────────────────────────────
486
637
  console.log(chalk.yellow('\n All automatic methods failed.'));
487
638
  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'));
639
+ console.log(chalk.gray(' • certbot: https://certbot.eff.org/instructions?ws=other&os=windows'));
640
+ console.log(chalk.gray(' • win-acme: https://github.com/win-acme/win-acme/releases'));
490
641
  console.log(chalk.gray(' Then transfer to this server and use "Specify local installer" below.\n'));
491
642
 
492
643
  let localChoice;
@@ -495,10 +646,29 @@ async function installCertbot() {
495
646
  type: 'list',
496
647
  name: 'localChoice',
497
648
  message: 'What would you like to do?',
498
- choices: ['Specify local installer path', 'Cancel'],
649
+ choices: [
650
+ 'Open download page in browser',
651
+ 'Specify local installer path',
652
+ 'Cancel'
653
+ ],
499
654
  }]));
500
655
  } catch { return { success: false }; }
501
656
 
657
+ if (localChoice === 'Open download page in browser') {
658
+ console.log(chalk.cyan('\n Opening download pages in browser...'));
659
+ console.log(chalk.gray(' Download either win-acme.zip or certbot installer, then re-run this tool.\n'));
660
+
661
+ // Open win-acme releases
662
+ await run('Start-Process "https://github.com/win-acme/win-acme/releases/latest"');
663
+ // Also open EFF certbot page
664
+ await run('Start-Process "https://certbot.eff.org/instructions?ws=other&os=windows"');
665
+
666
+ console.log(chalk.green('✓ Browser opened. Download the installer, then run:'));
667
+ console.log(chalk.cyan(' easy-devops → SSL Manager → Install certbot → Specify local installer path\n'));
668
+
669
+ return { success: false };
670
+ }
671
+
502
672
  if (localChoice === 'Specify local installer path') {
503
673
  let localPath;
504
674
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "easy-devops",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
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",