easy-devops 0.1.9 → 0.2.1

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.
@@ -15,6 +15,7 @@ import inquirer from 'inquirer';
15
15
  import ora from 'ora';
16
16
  import fs from 'fs/promises';
17
17
  import path from 'path';
18
+ import { fileURLToPath } from 'url';
18
19
  import { run, runLive } from '../../core/shell.js';
19
20
  import { loadConfig } from '../../core/config.js';
20
21
 
@@ -319,33 +320,30 @@ async function installCertbot() {
319
320
  // Enable all TLS versions (1.0, 1.1, 1.2, 1.3) for maximum compatibility
320
321
  const tlsSetup = `[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13`;
321
322
 
323
+ // Helper to safely run a command and catch EPERM errors
324
+ const safeRun = async (cmd, opts) => {
325
+ try {
326
+ return await run(cmd, opts);
327
+ } catch (err) {
328
+ // Re-throw EPERM so caller can handle Windows Defender blocks
329
+ if (err.code === 'EPERM' || err.message?.includes('EPERM')) {
330
+ throw err;
331
+ }
332
+ return { success: false, stdout: '', stderr: err.message, exitCode: null };
333
+ }
334
+ };
335
+
322
336
  // Method 1: Invoke-WebRequest with all TLS versions
323
- let r = await run(
337
+ let r = await safeRun(
324
338
  `${tlsSetup}; $ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '${safeUrl}' -OutFile '${safeDest}' -UseBasicParsing -TimeoutSec 120`,
325
339
  { timeout: 130000 },
326
340
  );
327
341
  if (r.success) return true;
328
342
  if (showErrors && r.stderr) lastDownloadError = `Invoke-WebRequest: ${r.stderr}`;
329
343
 
330
- // Method 2: WebClient with TLS
331
- r = await run(
332
- `${tlsSetup}; (New-Object System.Net.WebClient).DownloadFile('${safeUrl}','${safeDest}')`,
333
- { timeout: 130000 },
334
- );
335
- if (r.success) return true;
336
- if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `WebClient: ${r.stderr}`;
337
-
338
- // Method 3: BITS Transfer
339
- r = await run(
340
- `Import-Module BitsTransfer -ErrorAction SilentlyContinue; Start-BitsTransfer -Source '${safeUrl}' -Destination '${safeDest}' -ErrorAction Stop`,
341
- { timeout: 130000 },
342
- );
343
- if (r.success) return true;
344
- if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `BITS: ${r.stderr}`;
345
-
346
- // Method 4: curl.exe (works on Windows 10+)
344
+ // Method 2: curl.exe (works on Windows 10+, often bypasses AV detection)
347
345
  if (hasCurl) {
348
- r = await run(
346
+ r = await safeRun(
349
347
  `curl.exe -L --ssl-no-revoke --max-time 120 -o '${safeDest}' '${safeUrl}'`,
350
348
  { timeout: 130000 },
351
349
  );
@@ -353,61 +351,33 @@ async function installCertbot() {
353
351
  if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `curl: ${r.stderr}`;
354
352
  }
355
353
 
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)`,
354
+ // Method 3: WebClient with TLS
355
+ r = await safeRun(
356
+ `${tlsSetup}; (New-Object System.Net.WebClient).DownloadFile('${safeUrl}','${safeDest}')`,
359
357
  { timeout: 130000 },
360
358
  );
361
359
  if (r.success) return true;
362
- if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `HttpClient: ${r.stderr}`;
360
+ if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `WebClient: ${r.stderr}`;
363
361
 
364
- // Method 6: Certutil (built-in Windows tool)
365
- r = await run(
366
- `certutil.exe -urlcache -split -f "${safeUrl}" "${safeDest}"`,
362
+ // Method 4: HttpClient with custom handler
363
+ r = await safeRun(
364
+ `${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)`,
367
365
  { timeout: 130000 },
368
366
  );
369
367
  if (r.success) return true;
370
- if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `certutil: ${r.stderr}`;
368
+ if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `HttpClient: ${r.stderr}`;
371
369
 
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)`,
370
+ // Method 5: BITS Transfer (background transfers)
371
+ r = await safeRun(
372
+ `Import-Module BitsTransfer -ErrorAction SilentlyContinue; Start-BitsTransfer -Source '${safeUrl}' -Destination '${safeDest}' -ErrorAction Stop`,
375
373
  { timeout: 130000 },
376
374
  );
377
375
  if (r.success) return true;
378
- if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `WebRequest: ${r.stderr}`;
379
-
380
- return false;
381
- }
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 = [];
376
+ if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `BITS: ${r.stderr}`;
392
377
 
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
- }
378
+ // Note: certutil.exe removed - it triggers Trojan:Win32/Ceprolad.A detection
399
379
 
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;
380
+ return false;
411
381
  }
412
382
 
413
383
  async function runNsisInstaller(exePath) {
@@ -432,70 +402,62 @@ async function installCertbot() {
432
402
 
433
403
  if (!wingetAvailable) {
434
404
  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.'));
405
+ console.log(chalk.gray(' winget (Windows Package Manager) provides the easiest installation method.'));
436
406
 
437
407
  let installWinget;
438
408
  try {
439
409
  ({ installWinget } = await inquirer.prompt([{
440
410
  type: 'confirm',
441
411
  name: 'installWinget',
442
- message: 'Would you like to install winget (App Installer) from Microsoft Store?',
412
+ message: 'Would you like to install winget automatically?',
443
413
  default: true,
444
414
  }]));
445
415
  } catch { /* user cancelled */ }
446
416
 
447
417
  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'));
418
+ console.log(chalk.cyan('\n Opening winget installer in a new window...'));
419
+ console.log(chalk.gray(' The installer will run in a separate PowerShell window.'));
420
+ console.log(chalk.gray(' Please complete the installation in that window.\n'));
421
+
422
+ // Use the embedded winget-install.ps1 script
423
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
424
+ const wingetScriptPath = path.join(scriptDir, '..', '..', 'lib', 'installer', 'winget-install.ps1');
425
+
426
+ // Check if the embedded script exists
427
+ const scriptExists = await run(`Test-Path "${wingetScriptPath}"`);
428
+ if (scriptExists.stdout.trim().toLowerCase() !== 'true') {
429
+ console.log(chalk.red('\n Embedded winget installer script not found.'));
430
+ console.log(chalk.gray(' Expected location: ' + wingetScriptPath + '\n'));
460
431
  } 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
- }
432
+ // Run the installer in a new PowerShell window with -NoExit so user can see output
433
+ // The script has a built-in -NoExit parameter we can use
434
+ await run(`Start-Process powershell.exe -ArgumentList '-ExecutionPolicy Bypass -NoProfile -File "${wingetScriptPath}" -NoExit' -Verb RunAs -Wait`);
465
435
 
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()) {
436
+ console.log(chalk.gray('\n Winget installer window has closed.'));
437
+ console.log(chalk.cyan(' Checking if winget is now available...\n'));
438
+
439
+ // Give a moment for PATH to update
440
+ await new Promise(res => setTimeout(res, 2000));
441
+
442
+ // Re-check for winget
443
+ const recheck = await run('where.exe winget 2>$null');
444
+ if (recheck.success && recheck.stdout.trim()) {
445
+ console.log(chalk.green(' winget installed successfully!\n'));
446
+ wingetAvailable = true;
447
+ } else {
448
+ // Try refreshing PATH from registry and check again
449
+ console.log(chalk.yellow(' winget not immediately detected. Refreshing PATH...'));
450
+ await run(`$env:PATH = [System.Environment]::GetEnvironmentVariable('PATH', 'Machine') + ';' + [System.Environment]::GetEnvironmentVariable('PATH', 'User')`);
451
+ await new Promise(res => setTimeout(res, 1000));
452
+
453
+ const recheck2 = await run('where.exe winget 2>$null');
454
+ if (recheck2.success && recheck2.stdout.trim()) {
491
455
  console.log(chalk.green('\n ✓ winget installed successfully!\n'));
492
456
  wingetAvailable = true;
493
457
  } 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'));
458
+ console.log(chalk.yellow('\n winget may have been installed but is not in PATH yet.'));
459
+ console.log(chalk.gray(' You may need to restart your terminal or computer.\n'));
496
460
  }
497
- } else {
498
- console.log(chalk.yellow('\n Could not download App Installer automatically.\n'));
499
461
  }
500
462
  }
501
463
 
@@ -508,28 +470,28 @@ async function installCertbot() {
508
470
  }
509
471
  }
510
472
 
511
- // ── Method 1: winget (win-acme - has better success rate) ─────────────────────
473
+ // ── Method 1: winget (certbot EFF - most reliable) ────────────────────────────
512
474
  if (wingetAvailable) {
513
- step('Trying winget (win-acme) ...');
514
- console.log(chalk.gray(' Running: winget install win-acme.win-acme\n'));
475
+ step('Trying winget (EFF.Certbot) ...');
476
+ console.log(chalk.gray(' Running: winget install EFF.Certbot\n'));
515
477
  const exitCode = await runLive(
516
- 'winget install -e --id win-acme.win-acme --accept-package-agreements --accept-source-agreements',
478
+ 'winget install -e --id EFF.Certbot --accept-package-agreements --accept-source-agreements',
517
479
  { timeout: 180000 },
518
480
  );
519
- if (exitCode === 0 || await verifyWinAcme()) return { success: true, client: 'winacme' };
520
- console.log(chalk.yellow(' winget win-acme failed, trying next...\n'));
481
+ if (exitCode === 0 || await verifyCertbot()) return { success: true };
482
+ console.log(chalk.yellow(' winget certbot failed, trying next...\n'));
521
483
  }
522
484
 
523
- // ── Method 2: winget (certbot EFF) ────────────────────────────────────────────
485
+ // ── Method 2: winget (win-acme) ───────────────────────────────────────────────
524
486
  if (wingetAvailable) {
525
- step('Trying winget (EFF.Certbot) ...');
526
- console.log(chalk.gray(' Running: winget install EFF.Certbot\n'));
487
+ step('Trying winget (win-acme) ...');
488
+ console.log(chalk.gray(' Running: winget install win-acme.win-acme\n'));
527
489
  const exitCode = await runLive(
528
- 'winget install -e --id EFF.Certbot --accept-package-agreements --accept-source-agreements',
490
+ 'winget install -e --id win-acme.win-acme --accept-package-agreements --accept-source-agreements',
529
491
  { timeout: 180000 },
530
492
  );
531
- if (exitCode === 0 || await verifyCertbot()) return { success: true };
532
- console.log(chalk.yellow(' winget certbot failed, trying next...\n'));
493
+ if (exitCode === 0 || await verifyWinAcme()) return { success: true, client: 'winacme' };
494
+ console.log(chalk.yellow(' winget win-acme failed, trying next...\n'));
533
495
  }
534
496
 
535
497
  // ── Method 3: Chocolatey (win-acme) ───────────────────────────────────────────
@@ -572,9 +534,6 @@ async function installCertbot() {
572
534
  // ── Method 7: Direct download win-acme (ZIP - smaller, no installer) ──────────
573
535
  const WINACME_DEST = 'C:\\Program Files\\win-acme';
574
536
 
575
- // Test network before attempting downloads
576
- await testNetworkConnectivity();
577
-
578
537
  step('Downloading win-acme from GitHub ...');
579
538
  const winAcmeUrls = [
580
539
  'https://github.com/win-acme/win-acme/releases/latest/download/win-acme.zip',
@@ -587,7 +546,14 @@ async function installCertbot() {
587
546
 
588
547
  const zipDest = `$env:TEMP\\win-acme.zip`;
589
548
  lastDownloadError = '';
590
- if (await downloadFile(url, zipDest, true)) {
549
+ let dlOk = false;
550
+ try { dlOk = await downloadFile(url, zipDest, true); } catch (err) {
551
+ if (err.code === 'EPERM' || err.message?.includes('EPERM')) {
552
+ console.log(chalk.red('Windows Defender blocked this download.'));
553
+ console.log(chalk.gray('Use the manual installer option or add a Windows Defender exclusion.'));
554
+ }
555
+ }
556
+ if (dlOk) {
591
557
  console.log(chalk.gray(' Extracting win-acme ...\n'));
592
558
  await run(`New-Item -ItemType Directory -Force -Path '${WINACME_DEST}'`);
593
559
  await run(`Expand-Archive -Path '${zipDest}' -DestinationPath '${WINACME_DEST}' -Force`);
@@ -619,7 +585,14 @@ async function installCertbot() {
619
585
  step(`Downloading certbot installer from ${hostname} ...`);
620
586
 
621
587
  lastDownloadError = '';
622
- if (await downloadFile(url, INSTALLER_DEST, true)) {
588
+ let dlOk = false;
589
+ try { dlOk = await downloadFile(url, INSTALLER_DEST, true); } catch (err) {
590
+ if (err.code === 'EPERM' || err.message?.includes('EPERM')) {
591
+ console.log(chalk.red('Windows Defender blocked this download.'));
592
+ console.log(chalk.gray('Use the manual installer option or add a Windows Defender exclusion.'));
593
+ }
594
+ }
595
+ if (dlOk) {
623
596
  console.log(chalk.gray(' Running installer silently ...\n'));
624
597
  const ok = await runNsisInstaller(INSTALLER_DEST);
625
598
  await run(`Remove-Item -Force '${INSTALLER_DEST}' -ErrorAction SilentlyContinue`);
@@ -664,7 +637,7 @@ async function installCertbot() {
664
637
  await run('Start-Process "https://certbot.eff.org/instructions?ws=other&os=windows"');
665
638
 
666
639
  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'));
640
+ console.log(chalk.cyan(' easy-devops → SSL Manager → Install certbot → Specify local installer path\n'));
668
641
 
669
642
  return { success: false };
670
643
  }