easy-devops 0.1.6 → 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 +215 -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,133 @@ 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
|
-
// ── Method 1:
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
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'));
|
|
336
382
|
}
|
|
337
383
|
|
|
338
|
-
// ── Method 2: winget
|
|
384
|
+
// ── Method 2: winget (certbot EFF) ────────────────────────────────────────────
|
|
339
385
|
if ((await run('where.exe winget 2>$null')).success) {
|
|
340
|
-
step('Trying winget ...');
|
|
386
|
+
step('Trying winget (EFF.Certbot) ...');
|
|
341
387
|
const exitCode = await runLive(
|
|
342
388
|
'winget install -e --id EFF.Certbot --accept-package-agreements --accept-source-agreements',
|
|
343
389
|
{ timeout: 180000 },
|
|
344
390
|
);
|
|
345
391
|
if (exitCode === 0 || await verifyCertbot()) return { success: true };
|
|
346
|
-
console.log(chalk.yellow('
|
|
392
|
+
console.log(chalk.yellow(' winget certbot failed, trying next...\n'));
|
|
393
|
+
}
|
|
394
|
+
|
|
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'));
|
|
347
401
|
}
|
|
348
402
|
|
|
349
|
-
// ── Method
|
|
403
|
+
// ── Method 4: Chocolatey (certbot) ────────────────────────────────────────────
|
|
350
404
|
if ((await run('where.exe choco 2>$null')).success) {
|
|
351
|
-
step('Trying Chocolatey ...');
|
|
405
|
+
step('Trying Chocolatey (certbot) ...');
|
|
352
406
|
const exitCode = await runLive('choco install certbot -y', { timeout: 180000 });
|
|
353
407
|
if (exitCode === 0 || await verifyCertbot()) return { success: true };
|
|
354
|
-
console.log(chalk.yellow('
|
|
408
|
+
console.log(chalk.yellow(' Chocolatey certbot failed, trying next...\n'));
|
|
355
409
|
}
|
|
356
410
|
|
|
357
|
-
// ── Method
|
|
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) ────────────────────────────────────────────────
|
|
358
424
|
if ((await run('where.exe scoop 2>$null')).success) {
|
|
359
|
-
step('Trying Scoop ...');
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
+
}
|
|
363
460
|
}
|
|
364
461
|
|
|
365
|
-
// ── Method
|
|
462
|
+
// ── Method 8: Direct download certbot installer ────────────────────────────────
|
|
366
463
|
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}`,
|
|
464
|
+
const INSTALLER_DEST = `$env:TEMP\\${INSTALLER_FILENAME}`;
|
|
465
|
+
const certbotUrls = [
|
|
370
466
|
`https://dl.eff.org/${INSTALLER_FILENAME}`,
|
|
467
|
+
`https://github.com/certbot/certbot/releases/latest/download/${INSTALLER_FILENAME}`,
|
|
371
468
|
];
|
|
372
469
|
|
|
373
|
-
for (const url of
|
|
374
|
-
const
|
|
375
|
-
step(`Downloading installer from ${
|
|
470
|
+
for (const url of certbotUrls) {
|
|
471
|
+
const hostname = new URL(url).hostname;
|
|
472
|
+
step(`Downloading certbot installer from ${hostname} ...`);
|
|
473
|
+
|
|
376
474
|
if (await downloadFile(url, INSTALLER_DEST)) {
|
|
377
|
-
console.log(chalk.gray('
|
|
475
|
+
console.log(chalk.gray(' Running installer silently ...\n'));
|
|
378
476
|
const ok = await runNsisInstaller(INSTALLER_DEST);
|
|
379
477
|
await run(`Remove-Item -Force '${INSTALLER_DEST}' -ErrorAction SilentlyContinue`);
|
|
380
478
|
if (ok) return { success: true };
|
|
381
|
-
console.log(chalk.yellow('
|
|
479
|
+
console.log(chalk.yellow(' Installer ran but certbot not detected, trying next...\n'));
|
|
382
480
|
} else {
|
|
383
|
-
console.log(chalk.yellow(`
|
|
481
|
+
console.log(chalk.yellow(` Could not download from ${hostname}`));
|
|
384
482
|
}
|
|
385
483
|
}
|
|
386
484
|
|
|
387
|
-
// ── Method
|
|
388
|
-
console.log(chalk.yellow('\n
|
|
389
|
-
console.log(chalk.gray('
|
|
390
|
-
console.log(chalk.gray(
|
|
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'));
|
|
391
491
|
|
|
392
492
|
let localChoice;
|
|
393
493
|
try {
|
|
394
494
|
({ localChoice } = await inquirer.prompt([{
|
|
395
|
-
type:
|
|
396
|
-
name:
|
|
495
|
+
type: 'list',
|
|
496
|
+
name: 'localChoice',
|
|
397
497
|
message: 'What would you like to do?',
|
|
398
498
|
choices: ['Specify local installer path', 'Cancel'],
|
|
399
499
|
}]));
|
|
@@ -403,23 +503,35 @@ async function installCertbot() {
|
|
|
403
503
|
let localPath;
|
|
404
504
|
try {
|
|
405
505
|
({ localPath } = await inquirer.prompt([{
|
|
406
|
-
type:
|
|
407
|
-
name:
|
|
408
|
-
message: 'Full path to
|
|
506
|
+
type: 'input',
|
|
507
|
+
name: 'localPath',
|
|
508
|
+
message: 'Full path to installer (.exe or .zip):',
|
|
409
509
|
validate: v => v.trim().length > 0 || 'Required',
|
|
410
510
|
}]));
|
|
411
511
|
} catch { return { success: false }; }
|
|
412
512
|
|
|
413
513
|
const exists = await run(`Test-Path '${localPath.trim()}'`);
|
|
414
514
|
if (exists.stdout.trim().toLowerCase() !== 'true') {
|
|
415
|
-
console.log(chalk.red(`
|
|
515
|
+
console.log(chalk.red(` File not found: ${localPath}\n`));
|
|
416
516
|
return { success: false };
|
|
417
517
|
}
|
|
418
518
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
if (
|
|
422
|
-
|
|
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'));
|
|
534
|
+
}
|
|
423
535
|
}
|
|
424
536
|
|
|
425
537
|
return { success: false };
|
|
@@ -435,11 +547,11 @@ export async function showSslManager() {
|
|
|
435
547
|
const certs = await listCerts(liveDir);
|
|
436
548
|
spinner.stop();
|
|
437
549
|
|
|
438
|
-
console.log(chalk.bold('\n
|
|
439
|
-
console.log(chalk.gray('
|
|
550
|
+
console.log(chalk.bold('\n SSL Manager'));
|
|
551
|
+
console.log(chalk.gray(' ' + '─'.repeat(40)));
|
|
440
552
|
|
|
441
553
|
if (certs.length === 0) {
|
|
442
|
-
console.log(chalk.gray('
|
|
554
|
+
console.log(chalk.gray(' No certificates found'));
|
|
443
555
|
} else {
|
|
444
556
|
for (const cert of certs) {
|
|
445
557
|
renderCertRow(cert);
|
|
@@ -472,12 +584,12 @@ export async function showSslManager() {
|
|
|
472
584
|
case 'Renew a certificate': {
|
|
473
585
|
const installed = await isCertbotInstalled();
|
|
474
586
|
if (!installed) {
|
|
475
|
-
console.log(chalk.yellow('\n
|
|
587
|
+
console.log(chalk.yellow('\n ⚠ certbot/win-acme not found — select "Install certbot" first\n'));
|
|
476
588
|
break;
|
|
477
589
|
}
|
|
478
590
|
|
|
479
591
|
if (certs.length === 0) {
|
|
480
|
-
console.log(chalk.gray('\n
|
|
592
|
+
console.log(chalk.gray('\n No certificates found to renew\n'));
|
|
481
593
|
break;
|
|
482
594
|
}
|
|
483
595
|
|
|
@@ -496,9 +608,9 @@ export async function showSslManager() {
|
|
|
496
608
|
|
|
497
609
|
const renewResult = await renewCert(selectedDomain);
|
|
498
610
|
if (renewResult.success) {
|
|
499
|
-
console.log(chalk.green('\n
|
|
611
|
+
console.log(chalk.green('\n ✓ Renewed successfully\n'));
|
|
500
612
|
} else {
|
|
501
|
-
console.log(chalk.red('\n
|
|
613
|
+
console.log(chalk.red('\n ✗ Renewal failed — see output above\n'));
|
|
502
614
|
}
|
|
503
615
|
break;
|
|
504
616
|
}
|
|
@@ -506,20 +618,20 @@ export async function showSslManager() {
|
|
|
506
618
|
case 'Renew all expiring (< 30 days)': {
|
|
507
619
|
const installed = await isCertbotInstalled();
|
|
508
620
|
if (!installed) {
|
|
509
|
-
console.log(chalk.yellow('\n
|
|
621
|
+
console.log(chalk.yellow('\n ⚠ certbot/win-acme not found — select "Install certbot" first\n'));
|
|
510
622
|
break;
|
|
511
623
|
}
|
|
512
624
|
|
|
513
625
|
const results = await renewExpiring(certs);
|
|
514
626
|
if (results.length === 0) {
|
|
515
|
-
console.log(chalk.gray('\n
|
|
627
|
+
console.log(chalk.gray('\n No certificates expiring within 30 days\n'));
|
|
516
628
|
} else {
|
|
517
629
|
console.log();
|
|
518
630
|
for (const r of results) {
|
|
519
631
|
if (r.success) {
|
|
520
|
-
console.log(`
|
|
632
|
+
console.log(` ${chalk.green('✓ ' + r.domain)}`);
|
|
521
633
|
} else {
|
|
522
|
-
console.log(`
|
|
634
|
+
console.log(` ${chalk.red('✗ ' + r.domain)}`);
|
|
523
635
|
}
|
|
524
636
|
}
|
|
525
637
|
console.log();
|
|
@@ -530,15 +642,17 @@ export async function showSslManager() {
|
|
|
530
642
|
case 'Install certbot': {
|
|
531
643
|
const alreadyInstalled = await isCertbotInstalled();
|
|
532
644
|
if (alreadyInstalled) {
|
|
533
|
-
|
|
645
|
+
const clientType = await getAcmeClientType();
|
|
646
|
+
console.log(chalk.gray(`\n ${clientType === 'winacme' ? 'win-acme' : 'certbot'} is already installed\n`));
|
|
534
647
|
break;
|
|
535
648
|
}
|
|
536
649
|
|
|
537
650
|
const installResult = await installCertbot();
|
|
538
651
|
if (installResult.success) {
|
|
539
|
-
|
|
652
|
+
const clientName = installResult.client === 'winacme' ? 'win-acme' : 'certbot';
|
|
653
|
+
console.log(chalk.green(`\n ✓ ${clientName} installed successfully\n`));
|
|
540
654
|
} else {
|
|
541
|
-
console.log(chalk.red('\n
|
|
655
|
+
console.log(chalk.red('\n ✗ Installation failed\n'));
|
|
542
656
|
}
|
|
543
657
|
break;
|
|
544
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