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.
- package/cli/managers/ssl-manager.js +99 -126
- package/lib/installer/winget-install.ps1 +1810 -0
- package/package.json +1 -1
|
@@ -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
|
|
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:
|
|
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
|
|
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
|
|
357
|
-
r = await
|
|
358
|
-
`${tlsSetup};
|
|
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 = `
|
|
360
|
+
if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `WebClient: ${r.stderr}`;
|
|
363
361
|
|
|
364
|
-
// Method
|
|
365
|
-
r = await
|
|
366
|
-
|
|
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 = `
|
|
368
|
+
if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `HttpClient: ${r.stderr}`;
|
|
371
369
|
|
|
372
|
-
// Method
|
|
373
|
-
r = await
|
|
374
|
-
|
|
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 = `
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
|
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
|
|
449
|
-
console.log(chalk.gray('
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
console.log(chalk.
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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
|
|
495
|
-
console.log(chalk.gray(' You may need to restart your terminal or
|
|
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 (
|
|
473
|
+
// ── Method 1: winget (certbot EFF - most reliable) ────────────────────────────
|
|
512
474
|
if (wingetAvailable) {
|
|
513
|
-
step('Trying winget (
|
|
514
|
-
console.log(chalk.gray(' Running: winget install
|
|
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
|
|
478
|
+
'winget install -e --id EFF.Certbot --accept-package-agreements --accept-source-agreements',
|
|
517
479
|
{ timeout: 180000 },
|
|
518
480
|
);
|
|
519
|
-
if (exitCode === 0 || await
|
|
520
|
-
console.log(chalk.yellow(' winget
|
|
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 (
|
|
485
|
+
// ── Method 2: winget (win-acme) ───────────────────────────────────────────────
|
|
524
486
|
if (wingetAvailable) {
|
|
525
|
-
step('Trying winget (
|
|
526
|
-
console.log(chalk.gray(' Running: winget install
|
|
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
|
|
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
|
|
532
|
-
console.log(chalk.yellow(' winget
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
}
|