easy-devops 0.1.6 → 0.1.8
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 +300 -101
- package/cli/menus/update.js +4 -1
- package/core/db.js +20 -1
- package/package.json +1 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* cli/managers/ssl-manager.js
|
|
3
3
|
*
|
|
4
|
-
* SSL Manager — view certificate status, renew certificates, install certbot.
|
|
4
|
+
* SSL Manager — view certificate status, renew certificates, install certbot/win-acme.
|
|
5
5
|
*
|
|
6
6
|
* Exported functions:
|
|
7
|
-
*
|
|
7
|
+
* - showSslManager() — interactive menu for managing SSL certificates
|
|
8
8
|
*
|
|
9
9
|
* All shell calls go through core/shell.js (run / runLive).
|
|
10
10
|
* Platform differences (Windows/Linux) are handled via isWindows guards.
|
|
@@ -52,13 +52,11 @@ async function parseCertExpiry(certPath) {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
// ───
|
|
56
|
-
//
|
|
57
|
-
// On Windows, checks PATH first, then the well-known install location used by
|
|
58
|
-
// the official EFF winget package (EFF.Certbot).
|
|
59
|
-
// Always returns a PS-safe invocation string (& "..." for full paths).
|
|
55
|
+
// ─── ACME Client Detection ────────────────────────────────────────────────────
|
|
56
|
+
// Supports both certbot and win-acme on Windows
|
|
60
57
|
|
|
61
58
|
const CERTBOT_WIN_EXE = 'C:\\Program Files\\Certbot\\bin\\certbot.exe';
|
|
59
|
+
const WINACME_EXE = 'C:\\Program Files\\win-acme\\wacs.exe';
|
|
62
60
|
|
|
63
61
|
async function getCertbotExe() {
|
|
64
62
|
if (!isWindows) {
|
|
@@ -66,19 +64,30 @@ async function getCertbotExe() {
|
|
|
66
64
|
return (r.exitCode === 0 && r.stdout.trim()) ? 'certbot' : null;
|
|
67
65
|
}
|
|
68
66
|
|
|
69
|
-
// 1.
|
|
67
|
+
// 1. certbot on PATH?
|
|
70
68
|
const pathResult = await run('where.exe certbot');
|
|
71
69
|
if (pathResult.exitCode === 0 && pathResult.stdout.trim()) {
|
|
72
70
|
return 'certbot';
|
|
73
71
|
}
|
|
74
72
|
|
|
75
|
-
// 2.
|
|
73
|
+
// 2. certbot well-known location
|
|
76
74
|
const exeCheck = await run(`Test-Path "${CERTBOT_WIN_EXE}"`);
|
|
77
75
|
if (exeCheck.stdout.trim().toLowerCase() === 'true') {
|
|
78
|
-
// Must use & "..." in PowerShell to invoke a path with spaces
|
|
79
76
|
return `& "${CERTBOT_WIN_EXE}"`;
|
|
80
77
|
}
|
|
81
78
|
|
|
79
|
+
// 3. win-acme on PATH?
|
|
80
|
+
const wacsPathResult = await run('where.exe wacs');
|
|
81
|
+
if (wacsPathResult.exitCode === 0 && wacsPathResult.stdout.trim()) {
|
|
82
|
+
return 'wacs';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 4. win-acme well-known location
|
|
86
|
+
const wacsCheck = await run(`Test-Path "${WINACME_EXE}"`);
|
|
87
|
+
if (wacsCheck.stdout.trim().toLowerCase() === 'true') {
|
|
88
|
+
return `& "${WINACME_EXE}"`;
|
|
89
|
+
}
|
|
90
|
+
|
|
82
91
|
return null;
|
|
83
92
|
}
|
|
84
93
|
|
|
@@ -86,6 +95,13 @@ async function isCertbotInstalled() {
|
|
|
86
95
|
return (await getCertbotExe()) !== null;
|
|
87
96
|
}
|
|
88
97
|
|
|
98
|
+
async function getAcmeClientType() {
|
|
99
|
+
const exe = await getCertbotExe();
|
|
100
|
+
if (!exe) return null;
|
|
101
|
+
if (exe.includes('wacs')) return 'winacme';
|
|
102
|
+
return 'certbot';
|
|
103
|
+
}
|
|
104
|
+
|
|
89
105
|
// ─── isPort80Busy ─────────────────────────────────────────────────────────────
|
|
90
106
|
|
|
91
107
|
async function isPort80Busy() {
|
|
@@ -165,7 +181,7 @@ function renderCertRow(cert) {
|
|
|
165
181
|
const domainPadded = cert.domain.padEnd(35);
|
|
166
182
|
|
|
167
183
|
if (cert.status === 'error') {
|
|
168
|
-
console.log(`
|
|
184
|
+
console.log(` ${chalk.gray('❌')} ${chalk.gray(domainPadded)} ${chalk.gray('ERROR')}`);
|
|
169
185
|
return;
|
|
170
186
|
}
|
|
171
187
|
|
|
@@ -175,11 +191,11 @@ function renderCertRow(cert) {
|
|
|
175
191
|
const daysStr = cert.daysLeft !== null ? `${cert.daysLeft}d` : '—';
|
|
176
192
|
|
|
177
193
|
if (cert.status === 'healthy') {
|
|
178
|
-
console.log(`
|
|
194
|
+
console.log(` ${chalk.green('✅')} ${chalk.green(domainPadded)} ${chalk.green(daysStr.padEnd(6))} ${chalk.green(`(${expiryStr})`)}`);
|
|
179
195
|
} else if (cert.status === 'expiring') {
|
|
180
|
-
console.log(`
|
|
196
|
+
console.log(` ${chalk.yellow('⚠️')} ${chalk.yellow(domainPadded)} ${chalk.yellow(daysStr.padEnd(6))} ${chalk.yellow(`(${expiryStr})`)}`);
|
|
181
197
|
} else {
|
|
182
|
-
console.log(`
|
|
198
|
+
console.log(` ${chalk.red('❌')} ${chalk.red(domainPadded)} ${chalk.red(daysStr.padEnd(6))} ${chalk.red(`(${expiryStr})`)}`);
|
|
183
199
|
}
|
|
184
200
|
}
|
|
185
201
|
|
|
@@ -188,24 +204,33 @@ function renderCertRow(cert) {
|
|
|
188
204
|
async function renewCert(domain) {
|
|
189
205
|
const certbotExe = await getCertbotExe();
|
|
190
206
|
if (!certbotExe) {
|
|
191
|
-
console.log(chalk.red('\n
|
|
207
|
+
console.log(chalk.red('\n certbot/win-acme not found — install it first\n'));
|
|
192
208
|
return { domain, success: false, exitCode: null };
|
|
193
209
|
}
|
|
194
210
|
|
|
211
|
+
const clientType = await getAcmeClientType();
|
|
212
|
+
|
|
195
213
|
await stopNginx();
|
|
196
214
|
|
|
197
215
|
try {
|
|
198
216
|
const portCheck = await isPort80Busy();
|
|
199
217
|
if (portCheck.busy) {
|
|
200
|
-
console.log(chalk.yellow(`\n
|
|
201
|
-
console.log(chalk.yellow('
|
|
218
|
+
console.log(chalk.yellow(`\n ⚠ Port 80 is in use: ${portCheck.detail}`));
|
|
219
|
+
console.log(chalk.yellow(' Stop that process before renewing.\n'));
|
|
202
220
|
return { domain, success: false, exitCode: null };
|
|
203
221
|
}
|
|
204
222
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
223
|
+
let cmd;
|
|
224
|
+
if (clientType === 'winacme') {
|
|
225
|
+
// win-acme interactive mode - needs manual input
|
|
226
|
+
console.log(chalk.cyan('\n win-acme will open. Follow the prompts to renew the certificate.'));
|
|
227
|
+
console.log(chalk.cyan(' Select: N) Create new certificate → 2) Manual input → enter domain\n'));
|
|
228
|
+
cmd = certbotExe;
|
|
229
|
+
} else {
|
|
230
|
+
cmd = `${certbotExe} certonly --standalone -d "${domain}"`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const exitCode = await runLive(cmd, { timeout: 120000 });
|
|
209
234
|
return { domain, success: exitCode === 0, exitCode };
|
|
210
235
|
} finally {
|
|
211
236
|
await startNginx();
|
|
@@ -221,15 +246,21 @@ async function renewExpiring(certs) {
|
|
|
221
246
|
const certbotExe = await getCertbotExe();
|
|
222
247
|
if (!certbotExe) return [];
|
|
223
248
|
|
|
249
|
+
const clientType = await getAcmeClientType();
|
|
250
|
+
|
|
224
251
|
await stopNginx();
|
|
225
252
|
|
|
226
253
|
const results = [];
|
|
227
254
|
try {
|
|
228
255
|
for (const cert of expiring) {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
{
|
|
232
|
-
|
|
256
|
+
let cmd;
|
|
257
|
+
if (clientType === 'winacme') {
|
|
258
|
+
console.log(chalk.cyan(`\n Renewing ${cert.domain} with win-acme - follow the prompts\n`));
|
|
259
|
+
cmd = certbotExe;
|
|
260
|
+
} else {
|
|
261
|
+
cmd = `${certbotExe} certonly --standalone -d "${cert.domain}"`;
|
|
262
|
+
}
|
|
263
|
+
const exitCode = await runLive(cmd, { timeout: 120000 });
|
|
233
264
|
results.push({ domain: cert.domain, success: exitCode === 0, exitCode });
|
|
234
265
|
}
|
|
235
266
|
} finally {
|
|
@@ -264,44 +295,60 @@ async function installCertbot() {
|
|
|
264
295
|
return false;
|
|
265
296
|
}
|
|
266
297
|
|
|
267
|
-
|
|
298
|
+
async function verifyWinAcme() {
|
|
299
|
+
const paths = [
|
|
300
|
+
WINACME_EXE,
|
|
301
|
+
'C:\\Program Files (x86)\\win-acme\\wacs.exe',
|
|
302
|
+
'C:\\win-acme\\wacs.exe',
|
|
303
|
+
];
|
|
304
|
+
for (const p of paths) {
|
|
305
|
+
const r = await run(`Test-Path '${p}'`);
|
|
306
|
+
if (r.stdout.trim().toLowerCase() === 'true') return true;
|
|
307
|
+
}
|
|
308
|
+
const whereResult = await run('where.exe wacs 2>$null');
|
|
309
|
+
return whereResult.success && whereResult.stdout.trim();
|
|
310
|
+
}
|
|
311
|
+
|
|
268
312
|
const hasCurl = (await run('where.exe curl.exe 2>$null')).success;
|
|
269
313
|
|
|
270
314
|
async function downloadFile(url, dest) {
|
|
271
|
-
|
|
315
|
+
const safeUrl = url.replace(/'/g, "''");
|
|
316
|
+
const safeDest = dest.replace(/'/g, "''");
|
|
317
|
+
|
|
318
|
+
// Method 1: Invoke-WebRequest with TLS 1.2
|
|
272
319
|
let r = await run(
|
|
273
|
-
`[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '${
|
|
274
|
-
{ timeout:
|
|
320
|
+
`[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '${safeUrl}' -OutFile '${safeDest}' -UseBasicParsing -TimeoutSec 120`,
|
|
321
|
+
{ timeout: 130000 },
|
|
275
322
|
);
|
|
276
323
|
if (r.success) return true;
|
|
277
324
|
|
|
278
|
-
// 2
|
|
325
|
+
// Method 2: WebClient
|
|
279
326
|
r = await run(
|
|
280
|
-
`[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (New-Object System.Net.WebClient).DownloadFile('${
|
|
281
|
-
{ timeout:
|
|
327
|
+
`[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (New-Object System.Net.WebClient).DownloadFile('${safeUrl}','${safeDest}')`,
|
|
328
|
+
{ timeout: 130000 },
|
|
282
329
|
);
|
|
283
330
|
if (r.success) return true;
|
|
284
331
|
|
|
285
|
-
// 3
|
|
332
|
+
// Method 3: BITS
|
|
286
333
|
r = await run(
|
|
287
|
-
`Start-BitsTransfer -Source '${
|
|
288
|
-
{ timeout:
|
|
334
|
+
`Import-Module BitsTransfer -ErrorAction SilentlyContinue; Start-BitsTransfer -Source '${safeUrl}' -Destination '${safeDest}' -ErrorAction Stop`,
|
|
335
|
+
{ timeout: 130000 },
|
|
289
336
|
);
|
|
290
337
|
if (r.success) return true;
|
|
291
338
|
|
|
292
|
-
// 4
|
|
339
|
+
// Method 4: curl.exe
|
|
293
340
|
if (hasCurl) {
|
|
294
341
|
r = await run(
|
|
295
|
-
`curl.exe -L --ssl-no-revoke --silent --show-error --max-time
|
|
296
|
-
{ timeout:
|
|
342
|
+
`curl.exe -L --ssl-no-revoke --silent --show-error --max-time 120 -o '${safeDest}' '${safeUrl}'`,
|
|
343
|
+
{ timeout: 130000 },
|
|
297
344
|
);
|
|
298
345
|
if (r.success) return true;
|
|
299
346
|
}
|
|
300
347
|
|
|
301
|
-
// 5
|
|
348
|
+
// Method 5: HttpClient
|
|
302
349
|
r = await run(
|
|
303
|
-
`[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $hc=[System.Net.Http.HttpClient]::new(); $hc.DefaultRequestHeaders.Add('User-Agent','Mozilla/5.0'); $bytes=$hc.GetByteArrayAsync('${
|
|
304
|
-
{ timeout:
|
|
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)`,
|
|
351
|
+
{ timeout: 130000 },
|
|
305
352
|
);
|
|
306
353
|
if (r.success) return true;
|
|
307
354
|
|
|
@@ -320,80 +367,218 @@ async function installCertbot() {
|
|
|
320
367
|
let methodNum = 0;
|
|
321
368
|
function step(label) {
|
|
322
369
|
methodNum++;
|
|
323
|
-
console.log(chalk.gray(`\n
|
|
370
|
+
console.log(chalk.gray(`\n [${methodNum}] ${label}\n`));
|
|
324
371
|
}
|
|
325
372
|
|
|
326
|
-
// ──
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
373
|
+
// ── Check winget availability and offer to install if missing ─────────────────
|
|
374
|
+
let wingetAvailable = false;
|
|
375
|
+
const wingetCheck = await run('where.exe winget 2>$null');
|
|
376
|
+
wingetAvailable = wingetCheck.success && wingetCheck.stdout.trim();
|
|
377
|
+
|
|
378
|
+
if (!wingetAvailable) {
|
|
379
|
+
console.log(chalk.yellow('\n ⚠ winget is not installed on this system.'));
|
|
380
|
+
console.log(chalk.gray(' winget (Windows Package Manager) provides the easiest installation method.'));
|
|
381
|
+
|
|
382
|
+
let installWinget;
|
|
383
|
+
try {
|
|
384
|
+
({ installWinget } = await inquirer.prompt([{
|
|
385
|
+
type: 'confirm',
|
|
386
|
+
name: 'installWinget',
|
|
387
|
+
message: 'Would you like to install winget (App Installer) from Microsoft Store?',
|
|
388
|
+
default: true,
|
|
389
|
+
}]));
|
|
390
|
+
} catch { /* user cancelled */ }
|
|
391
|
+
|
|
392
|
+
if (installWinget) {
|
|
393
|
+
console.log(chalk.cyan('\n Opening Microsoft Store to install App Installer...'));
|
|
394
|
+
console.log(chalk.gray(' Please complete the installation, then run this command again.\n'));
|
|
395
|
+
|
|
396
|
+
// Try to open Microsoft Store
|
|
397
|
+
const storeOpened = await run(
|
|
398
|
+
'Start-Process "ms-windows-store://pdp/?ProductId=9NBLGGH4NNS1"',
|
|
399
|
+
{ timeout: 10000 }
|
|
400
|
+
).catch(() => ({ success: false }));
|
|
401
|
+
|
|
402
|
+
if (storeOpened.success) {
|
|
403
|
+
console.log(chalk.green(' Microsoft Store opened successfully.'));
|
|
404
|
+
console.log(chalk.gray(' After installing App Installer, winget will be available.\n'));
|
|
405
|
+
} else {
|
|
406
|
+
console.log(chalk.yellow(' Could not open Microsoft Store directly.'));
|
|
407
|
+
console.log(chalk.gray(' Please manually install "App Installer" from:'));
|
|
408
|
+
console.log(chalk.cyan(' https://apps.microsoft.com/store/detail/app-installer/9NBLGGH4NNS1\n'));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Optional: try direct download of App Installer
|
|
412
|
+
let tryDownload;
|
|
413
|
+
try {
|
|
414
|
+
({ tryDownload } = await inquirer.prompt([{
|
|
415
|
+
type: 'confirm',
|
|
416
|
+
name: 'tryDownload',
|
|
417
|
+
message: 'Would you like to try downloading App Installer directly?',
|
|
418
|
+
default: false,
|
|
419
|
+
}]));
|
|
420
|
+
} catch { tryDownload = false; }
|
|
421
|
+
|
|
422
|
+
if (tryDownload) {
|
|
423
|
+
const appInstallerUrl = 'https://aka.ms/getwinget';
|
|
424
|
+
const appInstallerDest = '$env:TEMP\\AppInstaller.msixbundle';
|
|
425
|
+
|
|
426
|
+
console.log(chalk.gray('\n Downloading App Installer...'));
|
|
427
|
+
const downloaded = await downloadFile(appInstallerUrl, appInstallerDest);
|
|
428
|
+
|
|
429
|
+
if (downloaded) {
|
|
430
|
+
console.log(chalk.gray(' Running App Installer...'));
|
|
431
|
+
await run(`Start-Process -FilePath '${appInstallerDest}' -Wait`, { timeout: 300000 });
|
|
432
|
+
|
|
433
|
+
// Re-check for winget
|
|
434
|
+
const recheck = await run('where.exe winget 2>$null');
|
|
435
|
+
if (recheck.success && recheck.stdout.trim()) {
|
|
436
|
+
console.log(chalk.green('\n ✓ winget installed successfully!\n'));
|
|
437
|
+
wingetAvailable = true;
|
|
438
|
+
} else {
|
|
439
|
+
console.log(chalk.yellow('\n App Installer ran but winget is not yet available.'));
|
|
440
|
+
console.log(chalk.gray(' You may need to restart your terminal or sign out/in.\n'));
|
|
441
|
+
}
|
|
442
|
+
} else {
|
|
443
|
+
console.log(chalk.yellow('\n Could not download App Installer automatically.\n'));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// If still no winget, continue with other methods
|
|
448
|
+
if (!wingetAvailable) {
|
|
449
|
+
console.log(chalk.gray(' Continuing with alternative installation methods...\n'));
|
|
450
|
+
}
|
|
451
|
+
} else {
|
|
452
|
+
console.log(chalk.gray(' Skipping winget installation. Using alternative methods...\n'));
|
|
335
453
|
}
|
|
336
454
|
}
|
|
337
455
|
|
|
338
|
-
// ── Method
|
|
339
|
-
if (
|
|
340
|
-
step('Trying winget ...');
|
|
456
|
+
// ── Method 1: winget (win-acme - has better success rate) ─────────────────────
|
|
457
|
+
if (wingetAvailable) {
|
|
458
|
+
step('Trying winget (win-acme) ...');
|
|
459
|
+
console.log(chalk.gray(' Running: winget install win-acme.win-acme\n'));
|
|
460
|
+
const exitCode = await runLive(
|
|
461
|
+
'winget install -e --id win-acme.win-acme --accept-package-agreements --accept-source-agreements',
|
|
462
|
+
{ timeout: 180000 },
|
|
463
|
+
);
|
|
464
|
+
if (exitCode === 0 || await verifyWinAcme()) return { success: true, client: 'winacme' };
|
|
465
|
+
console.log(chalk.yellow(' winget win-acme failed, trying next...\n'));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ── Method 2: winget (certbot EFF) ────────────────────────────────────────────
|
|
469
|
+
if (wingetAvailable) {
|
|
470
|
+
step('Trying winget (EFF.Certbot) ...');
|
|
471
|
+
console.log(chalk.gray(' Running: winget install EFF.Certbot\n'));
|
|
341
472
|
const exitCode = await runLive(
|
|
342
473
|
'winget install -e --id EFF.Certbot --accept-package-agreements --accept-source-agreements',
|
|
343
474
|
{ timeout: 180000 },
|
|
344
475
|
);
|
|
345
476
|
if (exitCode === 0 || await verifyCertbot()) return { success: true };
|
|
346
|
-
console.log(chalk.yellow('
|
|
477
|
+
console.log(chalk.yellow(' winget certbot failed, trying next...\n'));
|
|
347
478
|
}
|
|
348
479
|
|
|
349
|
-
// ── Method 3: Chocolatey
|
|
480
|
+
// ── Method 3: Chocolatey (win-acme) ───────────────────────────────────────────
|
|
350
481
|
if ((await run('where.exe choco 2>$null')).success) {
|
|
351
|
-
step('Trying Chocolatey ...');
|
|
482
|
+
step('Trying Chocolatey (win-acme) ...');
|
|
483
|
+
const exitCode = await runLive('choco install win-acme -y', { timeout: 180000 });
|
|
484
|
+
if (exitCode === 0 || await verifyWinAcme()) return { success: true, client: 'winacme' };
|
|
485
|
+
console.log(chalk.yellow(' Chocolatey win-acme failed, trying next...\n'));
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// ── Method 4: Chocolatey (certbot) ────────────────────────────────────────────
|
|
489
|
+
if ((await run('where.exe choco 2>$null')).success) {
|
|
490
|
+
step('Trying Chocolatey (certbot) ...');
|
|
352
491
|
const exitCode = await runLive('choco install certbot -y', { timeout: 180000 });
|
|
353
492
|
if (exitCode === 0 || await verifyCertbot()) return { success: true };
|
|
354
|
-
console.log(chalk.yellow('
|
|
493
|
+
console.log(chalk.yellow(' Chocolatey certbot failed, trying next...\n'));
|
|
355
494
|
}
|
|
356
495
|
|
|
357
|
-
// ── Method
|
|
496
|
+
// ── Method 5: pip (certbot) ───────────────────────────────────────────────────
|
|
497
|
+
for (const pip of ['pip', 'pip3']) {
|
|
498
|
+
const check = await run(`where.exe ${pip} 2>$null`);
|
|
499
|
+
if (check.success && check.stdout.trim()) {
|
|
500
|
+
step(`Trying ${pip} install certbot ...`);
|
|
501
|
+
const exitCode = await runLive(`${pip} install certbot`, { timeout: 180000 });
|
|
502
|
+
if (exitCode === 0 || await verifyCertbot()) return { success: true };
|
|
503
|
+
console.log(chalk.yellow(` ${pip} did not install certbot, trying next...\n`));
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ── Method 6: Scoop (win-acme) ────────────────────────────────────────────────
|
|
358
509
|
if ((await run('where.exe scoop 2>$null')).success) {
|
|
359
|
-
step('Trying Scoop ...');
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
510
|
+
step('Trying Scoop (win-acme) ...');
|
|
511
|
+
await runLive('scoop bucket add extras', { timeout: 60000 });
|
|
512
|
+
const exitCode = await runLive('scoop install win-acme', { timeout: 180000 });
|
|
513
|
+
if (exitCode === 0 || await verifyWinAcme()) return { success: true, client: 'winacme' };
|
|
514
|
+
console.log(chalk.yellow(' Scoop win-acme failed, trying next...\n'));
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ── Method 7: Direct download win-acme (ZIP - smaller, no installer) ──────────
|
|
518
|
+
const WINACME_DEST = 'C:\\Program Files\\win-acme';
|
|
519
|
+
|
|
520
|
+
step('Downloading win-acme from GitHub ...');
|
|
521
|
+
const winAcmeUrls = [
|
|
522
|
+
'https://github.com/win-acme/win-acme/releases/latest/download/win-acme.zip',
|
|
523
|
+
'https://github.com/win-acme/win-acme/releases/download/v2.2.9.1/win-acme.v2.2.9.1.zip',
|
|
524
|
+
];
|
|
525
|
+
|
|
526
|
+
for (const url of winAcmeUrls) {
|
|
527
|
+
const hostname = new URL(url).hostname;
|
|
528
|
+
console.log(chalk.gray(` Downloading from ${hostname} ...`));
|
|
529
|
+
|
|
530
|
+
const zipDest = `$env:TEMP\\win-acme.zip`;
|
|
531
|
+
if (await downloadFile(url, zipDest)) {
|
|
532
|
+
console.log(chalk.gray(' Extracting win-acme ...\n'));
|
|
533
|
+
await run(`New-Item -ItemType Directory -Force -Path '${WINACME_DEST}'`);
|
|
534
|
+
await run(`Expand-Archive -Path '${zipDest}' -DestinationPath '${WINACME_DEST}' -Force`);
|
|
535
|
+
await run(`Remove-Item -Force '${zipDest}' -ErrorAction SilentlyContinue`);
|
|
536
|
+
|
|
537
|
+
if (await verifyWinAcme()) {
|
|
538
|
+
console.log(chalk.green(` win-acme installed to ${WINACME_DEST}\n`));
|
|
539
|
+
return { success: true, client: 'winacme' };
|
|
540
|
+
}
|
|
541
|
+
console.log(chalk.yellow(' Extraction succeeded but verification failed, trying next...\n'));
|
|
542
|
+
} else {
|
|
543
|
+
console.log(chalk.yellow(` Could not download from ${hostname}`));
|
|
544
|
+
}
|
|
363
545
|
}
|
|
364
546
|
|
|
365
|
-
// ── Method
|
|
547
|
+
// ── Method 8: Direct download certbot installer ────────────────────────────────
|
|
366
548
|
const INSTALLER_FILENAME = 'certbot-beta-installer-win_amd64_signed.exe';
|
|
367
|
-
const INSTALLER_DEST
|
|
368
|
-
const
|
|
369
|
-
`https://github.com/certbot/certbot/releases/latest/download/${INSTALLER_FILENAME}`,
|
|
549
|
+
const INSTALLER_DEST = `$env:TEMP\\${INSTALLER_FILENAME}`;
|
|
550
|
+
const certbotUrls = [
|
|
370
551
|
`https://dl.eff.org/${INSTALLER_FILENAME}`,
|
|
552
|
+
`https://github.com/certbot/certbot/releases/latest/download/${INSTALLER_FILENAME}`,
|
|
371
553
|
];
|
|
372
554
|
|
|
373
|
-
for (const url of
|
|
374
|
-
const
|
|
375
|
-
step(`Downloading installer from ${
|
|
555
|
+
for (const url of certbotUrls) {
|
|
556
|
+
const hostname = new URL(url).hostname;
|
|
557
|
+
step(`Downloading certbot installer from ${hostname} ...`);
|
|
558
|
+
|
|
376
559
|
if (await downloadFile(url, INSTALLER_DEST)) {
|
|
377
|
-
console.log(chalk.gray('
|
|
560
|
+
console.log(chalk.gray(' Running installer silently ...\n'));
|
|
378
561
|
const ok = await runNsisInstaller(INSTALLER_DEST);
|
|
379
562
|
await run(`Remove-Item -Force '${INSTALLER_DEST}' -ErrorAction SilentlyContinue`);
|
|
380
563
|
if (ok) return { success: true };
|
|
381
|
-
console.log(chalk.yellow('
|
|
564
|
+
console.log(chalk.yellow(' Installer ran but certbot not detected, trying next...\n'));
|
|
382
565
|
} else {
|
|
383
|
-
console.log(chalk.yellow(`
|
|
566
|
+
console.log(chalk.yellow(` Could not download from ${hostname}`));
|
|
384
567
|
}
|
|
385
568
|
}
|
|
386
569
|
|
|
387
|
-
// ── Method
|
|
388
|
-
console.log(chalk.yellow('\n
|
|
389
|
-
console.log(chalk.gray('
|
|
390
|
-
console.log(chalk.gray(
|
|
570
|
+
// ── Method 9: Manual installer path ───────────────────────────────────────────
|
|
571
|
+
console.log(chalk.yellow('\n All automatic methods failed.'));
|
|
572
|
+
console.log(chalk.gray(' You can manually download one of these on another PC:'));
|
|
573
|
+
console.log(chalk.gray(' • certbot: https://certbot.eff.org/instructions?ws=other&os=windows'));
|
|
574
|
+
console.log(chalk.gray(' • win-acme: https://github.com/win-acme/win-acme/releases'));
|
|
575
|
+
console.log(chalk.gray(' Then transfer to this server and use "Specify local installer" below.\n'));
|
|
391
576
|
|
|
392
577
|
let localChoice;
|
|
393
578
|
try {
|
|
394
579
|
({ localChoice } = await inquirer.prompt([{
|
|
395
|
-
type:
|
|
396
|
-
name:
|
|
580
|
+
type: 'list',
|
|
581
|
+
name: 'localChoice',
|
|
397
582
|
message: 'What would you like to do?',
|
|
398
583
|
choices: ['Specify local installer path', 'Cancel'],
|
|
399
584
|
}]));
|
|
@@ -403,23 +588,35 @@ async function installCertbot() {
|
|
|
403
588
|
let localPath;
|
|
404
589
|
try {
|
|
405
590
|
({ localPath } = await inquirer.prompt([{
|
|
406
|
-
type:
|
|
407
|
-
name:
|
|
408
|
-
message: 'Full path to
|
|
591
|
+
type: 'input',
|
|
592
|
+
name: 'localPath',
|
|
593
|
+
message: 'Full path to installer (.exe or .zip):',
|
|
409
594
|
validate: v => v.trim().length > 0 || 'Required',
|
|
410
595
|
}]));
|
|
411
596
|
} catch { return { success: false }; }
|
|
412
597
|
|
|
413
598
|
const exists = await run(`Test-Path '${localPath.trim()}'`);
|
|
414
599
|
if (exists.stdout.trim().toLowerCase() !== 'true') {
|
|
415
|
-
console.log(chalk.red(`
|
|
600
|
+
console.log(chalk.red(` File not found: ${localPath}\n`));
|
|
416
601
|
return { success: false };
|
|
417
602
|
}
|
|
418
603
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
if (
|
|
422
|
-
|
|
604
|
+
const ext = path.extname(localPath.trim().toLowerCase());
|
|
605
|
+
|
|
606
|
+
if (ext === '.zip') {
|
|
607
|
+
step('Extracting ZIP archive ...');
|
|
608
|
+
await run(`New-Item -ItemType Directory -Force -Path '${WINACME_DEST}'`);
|
|
609
|
+
await run(`Expand-Archive -Path '${localPath.trim()}' -DestinationPath '${WINACME_DEST}' -Force`);
|
|
610
|
+
if (await verifyWinAcme()) {
|
|
611
|
+
return { success: true, client: 'winacme' };
|
|
612
|
+
}
|
|
613
|
+
console.log(chalk.red(' Extraction succeeded but win-acme not found.\n'));
|
|
614
|
+
} else {
|
|
615
|
+
step('Running installer silently ...');
|
|
616
|
+
const ok = await runNsisInstaller(localPath.trim());
|
|
617
|
+
if (ok) return { success: true };
|
|
618
|
+
console.log(chalk.red(' Installer ran but certbot was not detected.\n'));
|
|
619
|
+
}
|
|
423
620
|
}
|
|
424
621
|
|
|
425
622
|
return { success: false };
|
|
@@ -435,11 +632,11 @@ export async function showSslManager() {
|
|
|
435
632
|
const certs = await listCerts(liveDir);
|
|
436
633
|
spinner.stop();
|
|
437
634
|
|
|
438
|
-
console.log(chalk.bold('\n
|
|
439
|
-
console.log(chalk.gray('
|
|
635
|
+
console.log(chalk.bold('\n SSL Manager'));
|
|
636
|
+
console.log(chalk.gray(' ' + '─'.repeat(40)));
|
|
440
637
|
|
|
441
638
|
if (certs.length === 0) {
|
|
442
|
-
console.log(chalk.gray('
|
|
639
|
+
console.log(chalk.gray(' No certificates found'));
|
|
443
640
|
} else {
|
|
444
641
|
for (const cert of certs) {
|
|
445
642
|
renderCertRow(cert);
|
|
@@ -472,12 +669,12 @@ export async function showSslManager() {
|
|
|
472
669
|
case 'Renew a certificate': {
|
|
473
670
|
const installed = await isCertbotInstalled();
|
|
474
671
|
if (!installed) {
|
|
475
|
-
console.log(chalk.yellow('\n
|
|
672
|
+
console.log(chalk.yellow('\n ⚠ certbot/win-acme not found — select "Install certbot" first\n'));
|
|
476
673
|
break;
|
|
477
674
|
}
|
|
478
675
|
|
|
479
676
|
if (certs.length === 0) {
|
|
480
|
-
console.log(chalk.gray('\n
|
|
677
|
+
console.log(chalk.gray('\n No certificates found to renew\n'));
|
|
481
678
|
break;
|
|
482
679
|
}
|
|
483
680
|
|
|
@@ -496,9 +693,9 @@ export async function showSslManager() {
|
|
|
496
693
|
|
|
497
694
|
const renewResult = await renewCert(selectedDomain);
|
|
498
695
|
if (renewResult.success) {
|
|
499
|
-
console.log(chalk.green('\n
|
|
696
|
+
console.log(chalk.green('\n ✓ Renewed successfully\n'));
|
|
500
697
|
} else {
|
|
501
|
-
console.log(chalk.red('\n
|
|
698
|
+
console.log(chalk.red('\n ✗ Renewal failed — see output above\n'));
|
|
502
699
|
}
|
|
503
700
|
break;
|
|
504
701
|
}
|
|
@@ -506,20 +703,20 @@ export async function showSslManager() {
|
|
|
506
703
|
case 'Renew all expiring (< 30 days)': {
|
|
507
704
|
const installed = await isCertbotInstalled();
|
|
508
705
|
if (!installed) {
|
|
509
|
-
console.log(chalk.yellow('\n
|
|
706
|
+
console.log(chalk.yellow('\n ⚠ certbot/win-acme not found — select "Install certbot" first\n'));
|
|
510
707
|
break;
|
|
511
708
|
}
|
|
512
709
|
|
|
513
710
|
const results = await renewExpiring(certs);
|
|
514
711
|
if (results.length === 0) {
|
|
515
|
-
console.log(chalk.gray('\n
|
|
712
|
+
console.log(chalk.gray('\n No certificates expiring within 30 days\n'));
|
|
516
713
|
} else {
|
|
517
714
|
console.log();
|
|
518
715
|
for (const r of results) {
|
|
519
716
|
if (r.success) {
|
|
520
|
-
console.log(`
|
|
717
|
+
console.log(` ${chalk.green('✓ ' + r.domain)}`);
|
|
521
718
|
} else {
|
|
522
|
-
console.log(`
|
|
719
|
+
console.log(` ${chalk.red('✗ ' + r.domain)}`);
|
|
523
720
|
}
|
|
524
721
|
}
|
|
525
722
|
console.log();
|
|
@@ -530,15 +727,17 @@ export async function showSslManager() {
|
|
|
530
727
|
case 'Install certbot': {
|
|
531
728
|
const alreadyInstalled = await isCertbotInstalled();
|
|
532
729
|
if (alreadyInstalled) {
|
|
533
|
-
|
|
730
|
+
const clientType = await getAcmeClientType();
|
|
731
|
+
console.log(chalk.gray(`\n ${clientType === 'winacme' ? 'win-acme' : 'certbot'} is already installed\n`));
|
|
534
732
|
break;
|
|
535
733
|
}
|
|
536
734
|
|
|
537
735
|
const installResult = await installCertbot();
|
|
538
736
|
if (installResult.success) {
|
|
539
|
-
|
|
737
|
+
const clientName = installResult.client === 'winacme' ? 'win-acme' : 'certbot';
|
|
738
|
+
console.log(chalk.green(`\n ✓ ${clientName} installed successfully\n`));
|
|
540
739
|
} else {
|
|
541
|
-
console.log(chalk.red('\n
|
|
740
|
+
console.log(chalk.red('\n ✗ Installation failed\n'));
|
|
542
741
|
}
|
|
543
742
|
break;
|
|
544
743
|
}
|
package/cli/menus/update.js
CHANGED
|
@@ -21,7 +21,7 @@ import path from 'path';
|
|
|
21
21
|
import { fileURLToPath } from 'url';
|
|
22
22
|
import { createRequire } from 'module';
|
|
23
23
|
import { run } from '../../core/shell.js';
|
|
24
|
-
import { dbGet, dbSet, closeDb } from '../../core/db.js';
|
|
24
|
+
import { dbGet, dbSet, closeDb, initDb } from '../../core/db.js';
|
|
25
25
|
import { loadConfig } from '../../core/config.js';
|
|
26
26
|
import { getDashboardStatus, startDashboard, stopDashboard } from './dashboard.js';
|
|
27
27
|
|
|
@@ -108,6 +108,9 @@ async function performUpdate(latestVersion) {
|
|
|
108
108
|
|
|
109
109
|
sp.succeed(`Updated to v${latestVersion}`);
|
|
110
110
|
|
|
111
|
+
// Re-initialize the database connection after npm replaced the module
|
|
112
|
+
initDb();
|
|
113
|
+
|
|
111
114
|
// Step 5 — restart dashboard if it was running before
|
|
112
115
|
const saved = dbGet('update-pre-dashboard');
|
|
113
116
|
dbSet('update-pre-dashboard', null);
|
package/core/db.js
CHANGED
|
@@ -16,8 +16,13 @@ try {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
let db;
|
|
19
|
+
|
|
20
|
+
function createDbConnection() {
|
|
21
|
+
return new GoodDB(new SQLiteDriver({ path: DB_PATH }));
|
|
22
|
+
}
|
|
23
|
+
|
|
19
24
|
try {
|
|
20
|
-
db =
|
|
25
|
+
db = createDbConnection();
|
|
21
26
|
} catch (err) {
|
|
22
27
|
process.stderr.write(`[ERROR] Failed to initialize database at: ${DB_PATH}\n`);
|
|
23
28
|
process.stderr.write(`Hint: Check that the current user has write access to: ${DATA_DIR}\n`);
|
|
@@ -39,3 +44,17 @@ export function closeDb() {
|
|
|
39
44
|
db.driver?.db?.close?.();
|
|
40
45
|
} catch { /* ignore */ }
|
|
41
46
|
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Reinitializes the database connection after it was closed.
|
|
50
|
+
* Call this after closeDb() when you need to use the database again.
|
|
51
|
+
*/
|
|
52
|
+
export function initDb() {
|
|
53
|
+
try {
|
|
54
|
+
// Check if connection is still open
|
|
55
|
+
db.driver?.db?.prepare('SELECT 1');
|
|
56
|
+
} catch {
|
|
57
|
+
// Connection is closed, create a new one
|
|
58
|
+
db = createDbConnection();
|
|
59
|
+
}
|
|
60
|
+
}
|
package/package.json
CHANGED