easy-devops 0.1.5 → 0.1.7
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 +281 -94
- 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 {
|
|
@@ -249,7 +280,6 @@ async function installCertbot() {
|
|
|
249
280
|
|
|
250
281
|
// ── Shared helpers ────────────────────────────────────────────────────────────
|
|
251
282
|
|
|
252
|
-
// Check multiple possible certbot locations — some methods install to different paths
|
|
253
283
|
async function verifyCertbot() {
|
|
254
284
|
const whereResult = await run('where.exe certbot 2>$null');
|
|
255
285
|
if (whereResult.success && whereResult.stdout.trim()) return true;
|
|
@@ -265,90 +295,245 @@ async function installCertbot() {
|
|
|
265
295
|
return false;
|
|
266
296
|
}
|
|
267
297
|
|
|
268
|
-
|
|
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
|
+
|
|
269
312
|
const hasCurl = (await run('where.exe curl.exe 2>$null')).success;
|
|
313
|
+
|
|
270
314
|
async function downloadFile(url, dest) {
|
|
271
|
-
const
|
|
272
|
-
|
|
315
|
+
const safeUrl = url.replace(/'/g, "''");
|
|
316
|
+
const safeDest = dest.replace(/'/g, "''");
|
|
317
|
+
|
|
318
|
+
// Method 1: Invoke-WebRequest with TLS 1.2
|
|
319
|
+
let r = await run(
|
|
320
|
+
`[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '${safeUrl}' -OutFile '${safeDest}' -UseBasicParsing -TimeoutSec 120`,
|
|
321
|
+
{ timeout: 130000 },
|
|
322
|
+
);
|
|
323
|
+
if (r.success) return true;
|
|
324
|
+
|
|
325
|
+
// Method 2: WebClient
|
|
326
|
+
r = await run(
|
|
327
|
+
`[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; (New-Object System.Net.WebClient).DownloadFile('${safeUrl}','${safeDest}')`,
|
|
273
328
|
{ timeout: 130000 },
|
|
274
329
|
);
|
|
275
|
-
if (
|
|
330
|
+
if (r.success) return true;
|
|
331
|
+
|
|
332
|
+
// Method 3: BITS
|
|
333
|
+
r = await run(
|
|
334
|
+
`Import-Module BitsTransfer -ErrorAction SilentlyContinue; Start-BitsTransfer -Source '${safeUrl}' -Destination '${safeDest}' -ErrorAction Stop`,
|
|
335
|
+
{ timeout: 130000 },
|
|
336
|
+
);
|
|
337
|
+
if (r.success) return true;
|
|
338
|
+
|
|
339
|
+
// Method 4: curl.exe
|
|
276
340
|
if (hasCurl) {
|
|
277
|
-
|
|
278
|
-
|
|
341
|
+
r = await run(
|
|
342
|
+
`curl.exe -L --ssl-no-revoke --silent --show-error --max-time 120 -o '${safeDest}' '${safeUrl}'`,
|
|
343
|
+
{ timeout: 130000 },
|
|
344
|
+
);
|
|
345
|
+
if (r.success) return true;
|
|
279
346
|
}
|
|
347
|
+
|
|
348
|
+
// Method 5: HttpClient
|
|
349
|
+
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)`,
|
|
351
|
+
{ timeout: 130000 },
|
|
352
|
+
);
|
|
353
|
+
if (r.success) return true;
|
|
354
|
+
|
|
280
355
|
return false;
|
|
281
356
|
}
|
|
282
357
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
358
|
+
async function runNsisInstaller(exePath) {
|
|
359
|
+
await run(
|
|
360
|
+
`$p = Start-Process -FilePath '${exePath}' -ArgumentList '/S' -PassThru -Wait; $p.ExitCode`,
|
|
361
|
+
{ timeout: 120000 },
|
|
362
|
+
);
|
|
363
|
+
await new Promise(res => setTimeout(res, 4000));
|
|
364
|
+
return verifyCertbot();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
let methodNum = 0;
|
|
368
|
+
function step(label) {
|
|
369
|
+
methodNum++;
|
|
370
|
+
console.log(chalk.gray(`\n [${methodNum}] ${label}\n`));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ── Method 1: winget (win-acme - has better success rate) ─────────────────────
|
|
374
|
+
if ((await run('where.exe winget 2>$null')).success) {
|
|
375
|
+
step('Trying winget (win-acme) ...');
|
|
376
|
+
const exitCode = await runLive(
|
|
377
|
+
'winget install -e --id win-acme.win-acme --accept-package-agreements --accept-source-agreements',
|
|
378
|
+
{ timeout: 180000 },
|
|
379
|
+
);
|
|
380
|
+
if (exitCode === 0 || await verifyWinAcme()) return { success: true, client: 'winacme' };
|
|
381
|
+
console.log(chalk.yellow(' winget win-acme failed, trying next...\n'));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ── Method 2: winget (certbot EFF) ────────────────────────────────────────────
|
|
385
|
+
if ((await run('where.exe winget 2>$null')).success) {
|
|
386
|
+
step('Trying winget (EFF.Certbot) ...');
|
|
287
387
|
const exitCode = await runLive(
|
|
288
388
|
'winget install -e --id EFF.Certbot --accept-package-agreements --accept-source-agreements',
|
|
289
389
|
{ timeout: 180000 },
|
|
290
390
|
);
|
|
291
391
|
if (exitCode === 0 || await verifyCertbot()) return { success: true };
|
|
292
|
-
console.log(chalk.yellow('
|
|
392
|
+
console.log(chalk.yellow(' winget certbot failed, trying next...\n'));
|
|
293
393
|
}
|
|
294
394
|
|
|
295
|
-
// ── Method
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
395
|
+
// ── Method 3: Chocolatey (win-acme) ───────────────────────────────────────────
|
|
396
|
+
if ((await run('where.exe choco 2>$null')).success) {
|
|
397
|
+
step('Trying Chocolatey (win-acme) ...');
|
|
398
|
+
const exitCode = await runLive('choco install win-acme -y', { timeout: 180000 });
|
|
399
|
+
if (exitCode === 0 || await verifyWinAcme()) return { success: true, client: 'winacme' };
|
|
400
|
+
console.log(chalk.yellow(' Chocolatey win-acme failed, trying next...\n'));
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ── Method 4: Chocolatey (certbot) ────────────────────────────────────────────
|
|
404
|
+
if ((await run('where.exe choco 2>$null')).success) {
|
|
405
|
+
step('Trying Chocolatey (certbot) ...');
|
|
299
406
|
const exitCode = await runLive('choco install certbot -y', { timeout: 180000 });
|
|
300
407
|
if (exitCode === 0 || await verifyCertbot()) return { success: true };
|
|
301
|
-
console.log(chalk.yellow('
|
|
408
|
+
console.log(chalk.yellow(' Chocolatey certbot failed, trying next...\n'));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// ── Method 5: pip (certbot) ───────────────────────────────────────────────────
|
|
412
|
+
for (const pip of ['pip', 'pip3']) {
|
|
413
|
+
const check = await run(`where.exe ${pip} 2>$null`);
|
|
414
|
+
if (check.success && check.stdout.trim()) {
|
|
415
|
+
step(`Trying ${pip} install certbot ...`);
|
|
416
|
+
const exitCode = await runLive(`${pip} install certbot`, { timeout: 180000 });
|
|
417
|
+
if (exitCode === 0 || await verifyCertbot()) return { success: true };
|
|
418
|
+
console.log(chalk.yellow(` ${pip} did not install certbot, trying next...\n`));
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ── Method 6: Scoop (win-acme) ────────────────────────────────────────────────
|
|
424
|
+
if ((await run('where.exe scoop 2>$null')).success) {
|
|
425
|
+
step('Trying Scoop (win-acme) ...');
|
|
426
|
+
await runLive('scoop bucket add extras', { timeout: 60000 });
|
|
427
|
+
const exitCode = await runLive('scoop install win-acme', { timeout: 180000 });
|
|
428
|
+
if (exitCode === 0 || await verifyWinAcme()) return { success: true, client: 'winacme' };
|
|
429
|
+
console.log(chalk.yellow(' Scoop win-acme failed, trying next...\n'));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ── Method 7: Direct download win-acme (ZIP - smaller, no installer) ──────────
|
|
433
|
+
const WINACME_DEST = 'C:\\Program Files\\win-acme';
|
|
434
|
+
|
|
435
|
+
step('Downloading win-acme from GitHub ...');
|
|
436
|
+
const winAcmeUrls = [
|
|
437
|
+
'https://github.com/win-acme/win-acme/releases/latest/download/win-acme.zip',
|
|
438
|
+
'https://github.com/win-acme/win-acme/releases/download/v2.2.9.1/win-acme.v2.2.9.1.zip',
|
|
439
|
+
];
|
|
440
|
+
|
|
441
|
+
for (const url of winAcmeUrls) {
|
|
442
|
+
const hostname = new URL(url).hostname;
|
|
443
|
+
console.log(chalk.gray(` Downloading from ${hostname} ...`));
|
|
444
|
+
|
|
445
|
+
const zipDest = `$env:TEMP\\win-acme.zip`;
|
|
446
|
+
if (await downloadFile(url, zipDest)) {
|
|
447
|
+
console.log(chalk.gray(' Extracting win-acme ...\n'));
|
|
448
|
+
await run(`New-Item -ItemType Directory -Force -Path '${WINACME_DEST}'`);
|
|
449
|
+
await run(`Expand-Archive -Path '${zipDest}' -DestinationPath '${WINACME_DEST}' -Force`);
|
|
450
|
+
await run(`Remove-Item -Force '${zipDest}' -ErrorAction SilentlyContinue`);
|
|
451
|
+
|
|
452
|
+
if (await verifyWinAcme()) {
|
|
453
|
+
console.log(chalk.green(` win-acme installed to ${WINACME_DEST}\n`));
|
|
454
|
+
return { success: true, client: 'winacme' };
|
|
455
|
+
}
|
|
456
|
+
console.log(chalk.yellow(' Extraction succeeded but verification failed, trying next...\n'));
|
|
457
|
+
} else {
|
|
458
|
+
console.log(chalk.yellow(` Could not download from ${hostname}`));
|
|
459
|
+
}
|
|
302
460
|
}
|
|
303
461
|
|
|
304
|
-
// ── Method
|
|
462
|
+
// ── Method 8: Direct download certbot installer ────────────────────────────────
|
|
305
463
|
const INSTALLER_FILENAME = 'certbot-beta-installer-win_amd64_signed.exe';
|
|
306
|
-
const INSTALLER_DEST
|
|
307
|
-
const
|
|
308
|
-
`https://github.com/certbot/certbot/releases/latest/download/${INSTALLER_FILENAME}`,
|
|
464
|
+
const INSTALLER_DEST = `$env:TEMP\\${INSTALLER_FILENAME}`;
|
|
465
|
+
const certbotUrls = [
|
|
309
466
|
`https://dl.eff.org/${INSTALLER_FILENAME}`,
|
|
467
|
+
`https://github.com/certbot/certbot/releases/latest/download/${INSTALLER_FILENAME}`,
|
|
310
468
|
];
|
|
311
469
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
console.log(chalk.gray(`\n [3/4] Downloading certbot installer from ${label} ...\n`));
|
|
316
|
-
if (await downloadFile(url, INSTALLER_DEST)) { downloaded = true; break; }
|
|
317
|
-
console.log(chalk.yellow(` Failed from ${label}`));
|
|
318
|
-
}
|
|
470
|
+
for (const url of certbotUrls) {
|
|
471
|
+
const hostname = new URL(url).hostname;
|
|
472
|
+
step(`Downloading certbot installer from ${hostname} ...`);
|
|
319
473
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
await new Promise(r => setTimeout(r, 4000));
|
|
330
|
-
if (await verifyCertbot()) return { success: true };
|
|
331
|
-
console.log(chalk.yellow(' Installer ran but certbot not found, trying pip...\n'));
|
|
474
|
+
if (await downloadFile(url, INSTALLER_DEST)) {
|
|
475
|
+
console.log(chalk.gray(' Running installer silently ...\n'));
|
|
476
|
+
const ok = await runNsisInstaller(INSTALLER_DEST);
|
|
477
|
+
await run(`Remove-Item -Force '${INSTALLER_DEST}' -ErrorAction SilentlyContinue`);
|
|
478
|
+
if (ok) return { success: true };
|
|
479
|
+
console.log(chalk.yellow(' Installer ran but certbot not detected, trying next...\n'));
|
|
480
|
+
} else {
|
|
481
|
+
console.log(chalk.yellow(` Could not download from ${hostname}`));
|
|
482
|
+
}
|
|
332
483
|
}
|
|
333
484
|
|
|
334
|
-
// ── Method
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
485
|
+
// ── Method 9: Manual installer path ───────────────────────────────────────────
|
|
486
|
+
console.log(chalk.yellow('\n All automatic methods failed.'));
|
|
487
|
+
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'));
|
|
490
|
+
console.log(chalk.gray(' Then transfer to this server and use "Specify local installer" below.\n'));
|
|
491
|
+
|
|
492
|
+
let localChoice;
|
|
493
|
+
try {
|
|
494
|
+
({ localChoice } = await inquirer.prompt([{
|
|
495
|
+
type: 'list',
|
|
496
|
+
name: 'localChoice',
|
|
497
|
+
message: 'What would you like to do?',
|
|
498
|
+
choices: ['Specify local installer path', 'Cancel'],
|
|
499
|
+
}]));
|
|
500
|
+
} catch { return { success: false }; }
|
|
501
|
+
|
|
502
|
+
if (localChoice === 'Specify local installer path') {
|
|
503
|
+
let localPath;
|
|
504
|
+
try {
|
|
505
|
+
({ localPath } = await inquirer.prompt([{
|
|
506
|
+
type: 'input',
|
|
507
|
+
name: 'localPath',
|
|
508
|
+
message: 'Full path to installer (.exe or .zip):',
|
|
509
|
+
validate: v => v.trim().length > 0 || 'Required',
|
|
510
|
+
}]));
|
|
511
|
+
} catch { return { success: false }; }
|
|
512
|
+
|
|
513
|
+
const exists = await run(`Test-Path '${localPath.trim()}'`);
|
|
514
|
+
if (exists.stdout.trim().toLowerCase() !== 'true') {
|
|
515
|
+
console.log(chalk.red(` File not found: ${localPath}\n`));
|
|
516
|
+
return { success: false };
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const ext = path.extname(localPath.trim().toLowerCase());
|
|
520
|
+
|
|
521
|
+
if (ext === '.zip') {
|
|
522
|
+
step('Extracting ZIP archive ...');
|
|
523
|
+
await run(`New-Item -ItemType Directory -Force -Path '${WINACME_DEST}'`);
|
|
524
|
+
await run(`Expand-Archive -Path '${localPath.trim()}' -DestinationPath '${WINACME_DEST}' -Force`);
|
|
525
|
+
if (await verifyWinAcme()) {
|
|
526
|
+
return { success: true, client: 'winacme' };
|
|
527
|
+
}
|
|
528
|
+
console.log(chalk.red(' Extraction succeeded but win-acme not found.\n'));
|
|
529
|
+
} else {
|
|
530
|
+
step('Running installer silently ...');
|
|
531
|
+
const ok = await runNsisInstaller(localPath.trim());
|
|
532
|
+
if (ok) return { success: true };
|
|
533
|
+
console.log(chalk.red(' Installer ran but certbot was not detected.\n'));
|
|
347
534
|
}
|
|
348
535
|
}
|
|
349
536
|
|
|
350
|
-
// All methods exhausted
|
|
351
|
-
console.log(chalk.gray('\n Manual install: https://certbot.eff.org/instructions?ws=other&os=windows\n'));
|
|
352
537
|
return { success: false };
|
|
353
538
|
}
|
|
354
539
|
|
|
@@ -362,11 +547,11 @@ export async function showSslManager() {
|
|
|
362
547
|
const certs = await listCerts(liveDir);
|
|
363
548
|
spinner.stop();
|
|
364
549
|
|
|
365
|
-
console.log(chalk.bold('\n
|
|
366
|
-
console.log(chalk.gray('
|
|
550
|
+
console.log(chalk.bold('\n SSL Manager'));
|
|
551
|
+
console.log(chalk.gray(' ' + '─'.repeat(40)));
|
|
367
552
|
|
|
368
553
|
if (certs.length === 0) {
|
|
369
|
-
console.log(chalk.gray('
|
|
554
|
+
console.log(chalk.gray(' No certificates found'));
|
|
370
555
|
} else {
|
|
371
556
|
for (const cert of certs) {
|
|
372
557
|
renderCertRow(cert);
|
|
@@ -399,12 +584,12 @@ export async function showSslManager() {
|
|
|
399
584
|
case 'Renew a certificate': {
|
|
400
585
|
const installed = await isCertbotInstalled();
|
|
401
586
|
if (!installed) {
|
|
402
|
-
console.log(chalk.yellow('\n
|
|
587
|
+
console.log(chalk.yellow('\n ⚠ certbot/win-acme not found — select "Install certbot" first\n'));
|
|
403
588
|
break;
|
|
404
589
|
}
|
|
405
590
|
|
|
406
591
|
if (certs.length === 0) {
|
|
407
|
-
console.log(chalk.gray('\n
|
|
592
|
+
console.log(chalk.gray('\n No certificates found to renew\n'));
|
|
408
593
|
break;
|
|
409
594
|
}
|
|
410
595
|
|
|
@@ -423,9 +608,9 @@ export async function showSslManager() {
|
|
|
423
608
|
|
|
424
609
|
const renewResult = await renewCert(selectedDomain);
|
|
425
610
|
if (renewResult.success) {
|
|
426
|
-
console.log(chalk.green('\n
|
|
611
|
+
console.log(chalk.green('\n ✓ Renewed successfully\n'));
|
|
427
612
|
} else {
|
|
428
|
-
console.log(chalk.red('\n
|
|
613
|
+
console.log(chalk.red('\n ✗ Renewal failed — see output above\n'));
|
|
429
614
|
}
|
|
430
615
|
break;
|
|
431
616
|
}
|
|
@@ -433,20 +618,20 @@ export async function showSslManager() {
|
|
|
433
618
|
case 'Renew all expiring (< 30 days)': {
|
|
434
619
|
const installed = await isCertbotInstalled();
|
|
435
620
|
if (!installed) {
|
|
436
|
-
console.log(chalk.yellow('\n
|
|
621
|
+
console.log(chalk.yellow('\n ⚠ certbot/win-acme not found — select "Install certbot" first\n'));
|
|
437
622
|
break;
|
|
438
623
|
}
|
|
439
624
|
|
|
440
625
|
const results = await renewExpiring(certs);
|
|
441
626
|
if (results.length === 0) {
|
|
442
|
-
console.log(chalk.gray('\n
|
|
627
|
+
console.log(chalk.gray('\n No certificates expiring within 30 days\n'));
|
|
443
628
|
} else {
|
|
444
629
|
console.log();
|
|
445
630
|
for (const r of results) {
|
|
446
631
|
if (r.success) {
|
|
447
|
-
console.log(`
|
|
632
|
+
console.log(` ${chalk.green('✓ ' + r.domain)}`);
|
|
448
633
|
} else {
|
|
449
|
-
console.log(`
|
|
634
|
+
console.log(` ${chalk.red('✗ ' + r.domain)}`);
|
|
450
635
|
}
|
|
451
636
|
}
|
|
452
637
|
console.log();
|
|
@@ -457,15 +642,17 @@ export async function showSslManager() {
|
|
|
457
642
|
case 'Install certbot': {
|
|
458
643
|
const alreadyInstalled = await isCertbotInstalled();
|
|
459
644
|
if (alreadyInstalled) {
|
|
460
|
-
|
|
645
|
+
const clientType = await getAcmeClientType();
|
|
646
|
+
console.log(chalk.gray(`\n ${clientType === 'winacme' ? 'win-acme' : 'certbot'} is already installed\n`));
|
|
461
647
|
break;
|
|
462
648
|
}
|
|
463
649
|
|
|
464
650
|
const installResult = await installCertbot();
|
|
465
651
|
if (installResult.success) {
|
|
466
|
-
|
|
652
|
+
const clientName = installResult.client === 'winacme' ? 'win-acme' : 'certbot';
|
|
653
|
+
console.log(chalk.green(`\n ✓ ${clientName} installed successfully\n`));
|
|
467
654
|
} else {
|
|
468
|
-
console.log(chalk.red('\n
|
|
655
|
+
console.log(chalk.red('\n ✗ Installation failed\n'));
|
|
469
656
|
}
|
|
470
657
|
break;
|
|
471
658
|
}
|
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