easy-devops 0.2.0 → 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 +81 -121
- 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
|
|
|
@@ -340,23 +341,7 @@ async function installCertbot() {
|
|
|
340
341
|
if (r.success) return true;
|
|
341
342
|
if (showErrors && r.stderr) lastDownloadError = `Invoke-WebRequest: ${r.stderr}`;
|
|
342
343
|
|
|
343
|
-
// Method 2:
|
|
344
|
-
r = await safeRun(
|
|
345
|
-
`${tlsSetup}; (New-Object System.Net.WebClient).DownloadFile('${safeUrl}','${safeDest}')`,
|
|
346
|
-
{ timeout: 130000 },
|
|
347
|
-
);
|
|
348
|
-
if (r.success) return true;
|
|
349
|
-
if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `WebClient: ${r.stderr}`;
|
|
350
|
-
|
|
351
|
-
// Method 3: BITS Transfer
|
|
352
|
-
r = await safeRun(
|
|
353
|
-
`Import-Module BitsTransfer -ErrorAction SilentlyContinue; Start-BitsTransfer -Source '${safeUrl}' -Destination '${safeDest}' -ErrorAction Stop`,
|
|
354
|
-
{ timeout: 130000 },
|
|
355
|
-
);
|
|
356
|
-
if (r.success) return true;
|
|
357
|
-
if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `BITS: ${r.stderr}`;
|
|
358
|
-
|
|
359
|
-
// Method 4: curl.exe (works on Windows 10+)
|
|
344
|
+
// Method 2: curl.exe (works on Windows 10+, often bypasses AV detection)
|
|
360
345
|
if (hasCurl) {
|
|
361
346
|
r = await safeRun(
|
|
362
347
|
`curl.exe -L --ssl-no-revoke --max-time 120 -o '${safeDest}' '${safeUrl}'`,
|
|
@@ -366,61 +351,33 @@ async function installCertbot() {
|
|
|
366
351
|
if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `curl: ${r.stderr}`;
|
|
367
352
|
}
|
|
368
353
|
|
|
369
|
-
// Method
|
|
354
|
+
// Method 3: WebClient with TLS
|
|
370
355
|
r = await safeRun(
|
|
371
|
-
`${tlsSetup};
|
|
356
|
+
`${tlsSetup}; (New-Object System.Net.WebClient).DownloadFile('${safeUrl}','${safeDest}')`,
|
|
372
357
|
{ timeout: 130000 },
|
|
373
358
|
);
|
|
374
359
|
if (r.success) return true;
|
|
375
|
-
if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `
|
|
360
|
+
if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `WebClient: ${r.stderr}`;
|
|
376
361
|
|
|
377
|
-
// Method
|
|
362
|
+
// Method 4: HttpClient with custom handler
|
|
378
363
|
r = await safeRun(
|
|
379
|
-
|
|
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)`,
|
|
380
365
|
{ timeout: 130000 },
|
|
381
366
|
);
|
|
382
367
|
if (r.success) return true;
|
|
383
|
-
if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `
|
|
368
|
+
if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `HttpClient: ${r.stderr}`;
|
|
384
369
|
|
|
385
|
-
// Method
|
|
370
|
+
// Method 5: BITS Transfer (background transfers)
|
|
386
371
|
r = await safeRun(
|
|
387
|
-
|
|
372
|
+
`Import-Module BitsTransfer -ErrorAction SilentlyContinue; Start-BitsTransfer -Source '${safeUrl}' -Destination '${safeDest}' -ErrorAction Stop`,
|
|
388
373
|
{ timeout: 130000 },
|
|
389
374
|
);
|
|
390
375
|
if (r.success) return true;
|
|
391
|
-
if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `
|
|
392
|
-
|
|
393
|
-
return false;
|
|
394
|
-
}
|
|
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 = [];
|
|
376
|
+
if (showErrors && r.stderr && !lastDownloadError) lastDownloadError = `BITS: ${r.stderr}`;
|
|
405
377
|
|
|
406
|
-
|
|
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
|
-
}
|
|
378
|
+
// Note: certutil.exe removed - it triggers Trojan:Win32/Ceprolad.A detection
|
|
412
379
|
|
|
413
|
-
|
|
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;
|
|
380
|
+
return false;
|
|
424
381
|
}
|
|
425
382
|
|
|
426
383
|
async function runNsisInstaller(exePath) {
|
|
@@ -445,70 +402,62 @@ async function installCertbot() {
|
|
|
445
402
|
|
|
446
403
|
if (!wingetAvailable) {
|
|
447
404
|
console.log(chalk.yellow('\n ⚠ winget is not installed on this system.'));
|
|
448
|
-
console.log(chalk.gray('
|
|
405
|
+
console.log(chalk.gray(' winget (Windows Package Manager) provides the easiest installation method.'));
|
|
449
406
|
|
|
450
407
|
let installWinget;
|
|
451
408
|
try {
|
|
452
409
|
({ installWinget } = await inquirer.prompt([{
|
|
453
410
|
type: 'confirm',
|
|
454
411
|
name: 'installWinget',
|
|
455
|
-
message: 'Would you like to install winget
|
|
412
|
+
message: 'Would you like to install winget automatically?',
|
|
456
413
|
default: true,
|
|
457
414
|
}]));
|
|
458
415
|
} catch { /* user cancelled */ }
|
|
459
416
|
|
|
460
417
|
if (installWinget) {
|
|
461
|
-
console.log(chalk.cyan('\n Opening
|
|
462
|
-
console.log(chalk.gray('
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
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'));
|
|
473
431
|
} else {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
}
|
|
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`);
|
|
478
435
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
console.log(chalk.gray(' Running App Installer...'));
|
|
499
|
-
await run(`Start-Process -FilePath '${appInstallerDest}' -Wait`, { timeout: 300000 });
|
|
500
|
-
|
|
501
|
-
// Re-check for winget
|
|
502
|
-
const recheck = await run('where.exe winget 2>$null');
|
|
503
|
-
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()) {
|
|
504
455
|
console.log(chalk.green('\n ✓ winget installed successfully!\n'));
|
|
505
456
|
wingetAvailable = true;
|
|
506
457
|
} else {
|
|
507
|
-
console.log(chalk.yellow('\n
|
|
508
|
-
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'));
|
|
509
460
|
}
|
|
510
|
-
} else {
|
|
511
|
-
console.log(chalk.yellow('\n Could not download App Installer automatically.\n'));
|
|
512
461
|
}
|
|
513
462
|
}
|
|
514
463
|
|
|
@@ -521,28 +470,28 @@ async function installCertbot() {
|
|
|
521
470
|
}
|
|
522
471
|
}
|
|
523
472
|
|
|
524
|
-
// ── Method 1: winget (
|
|
473
|
+
// ── Method 1: winget (certbot EFF - most reliable) ────────────────────────────
|
|
525
474
|
if (wingetAvailable) {
|
|
526
|
-
step('Trying winget (
|
|
527
|
-
console.log(chalk.gray(' Running: winget install
|
|
475
|
+
step('Trying winget (EFF.Certbot) ...');
|
|
476
|
+
console.log(chalk.gray(' Running: winget install EFF.Certbot\n'));
|
|
528
477
|
const exitCode = await runLive(
|
|
529
|
-
'winget install -e --id
|
|
478
|
+
'winget install -e --id EFF.Certbot --accept-package-agreements --accept-source-agreements',
|
|
530
479
|
{ timeout: 180000 },
|
|
531
480
|
);
|
|
532
|
-
if (exitCode === 0 || await
|
|
533
|
-
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'));
|
|
534
483
|
}
|
|
535
484
|
|
|
536
|
-
// ── Method 2: winget (
|
|
485
|
+
// ── Method 2: winget (win-acme) ───────────────────────────────────────────────
|
|
537
486
|
if (wingetAvailable) {
|
|
538
|
-
step('Trying winget (
|
|
539
|
-
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'));
|
|
540
489
|
const exitCode = await runLive(
|
|
541
|
-
'winget install -e --id
|
|
490
|
+
'winget install -e --id win-acme.win-acme --accept-package-agreements --accept-source-agreements',
|
|
542
491
|
{ timeout: 180000 },
|
|
543
492
|
);
|
|
544
|
-
if (exitCode === 0 || await
|
|
545
|
-
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'));
|
|
546
495
|
}
|
|
547
496
|
|
|
548
497
|
// ── Method 3: Chocolatey (win-acme) ───────────────────────────────────────────
|
|
@@ -585,9 +534,6 @@ async function installCertbot() {
|
|
|
585
534
|
// ── Method 7: Direct download win-acme (ZIP - smaller, no installer) ──────────
|
|
586
535
|
const WINACME_DEST = 'C:\\Program Files\\win-acme';
|
|
587
536
|
|
|
588
|
-
// Test network before attempting downloads
|
|
589
|
-
try { await testNetworkConnectivity(); } catch (err) { console.log(chalk.yellow('Network test failed: ' + err.message)); }
|
|
590
|
-
|
|
591
537
|
step('Downloading win-acme from GitHub ...');
|
|
592
538
|
const winAcmeUrls = [
|
|
593
539
|
'https://github.com/win-acme/win-acme/releases/latest/download/win-acme.zip',
|
|
@@ -600,7 +546,14 @@ async function installCertbot() {
|
|
|
600
546
|
|
|
601
547
|
const zipDest = `$env:TEMP\\win-acme.zip`;
|
|
602
548
|
lastDownloadError = '';
|
|
603
|
-
let dlOk = false;
|
|
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) {
|
|
604
557
|
console.log(chalk.gray(' Extracting win-acme ...\n'));
|
|
605
558
|
await run(`New-Item -ItemType Directory -Force -Path '${WINACME_DEST}'`);
|
|
606
559
|
await run(`Expand-Archive -Path '${zipDest}' -DestinationPath '${WINACME_DEST}' -Force`);
|
|
@@ -632,7 +585,14 @@ async function installCertbot() {
|
|
|
632
585
|
step(`Downloading certbot installer from ${hostname} ...`);
|
|
633
586
|
|
|
634
587
|
lastDownloadError = '';
|
|
635
|
-
let dlOk = false;
|
|
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) {
|
|
636
596
|
console.log(chalk.gray(' Running installer silently ...\n'));
|
|
637
597
|
const ok = await runNsisInstaller(INSTALLER_DEST);
|
|
638
598
|
await run(`Remove-Item -Force '${INSTALLER_DEST}' -ErrorAction SilentlyContinue`);
|
|
@@ -677,7 +637,7 @@ async function installCertbot() {
|
|
|
677
637
|
await run('Start-Process "https://certbot.eff.org/instructions?ws=other&os=windows"');
|
|
678
638
|
|
|
679
639
|
console.log(chalk.green('✓ Browser opened. Download the installer, then run:'));
|
|
680
|
-
console.log(chalk.cyan('
|
|
640
|
+
console.log(chalk.cyan(' easy-devops → SSL Manager → Install certbot → Specify local installer path\n'));
|
|
681
641
|
|
|
682
642
|
return { success: false };
|
|
683
643
|
}
|