easy-devops 0.1.1 → 0.1.2
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.
|
@@ -15,6 +15,7 @@ import inquirer from 'inquirer';
|
|
|
15
15
|
import ora from 'ora';
|
|
16
16
|
import { run, runLive } from '../../core/shell.js';
|
|
17
17
|
import { loadConfig } from '../../core/config.js';
|
|
18
|
+
import { ensureNginxInclude } from '../../core/nginx-conf-generator.js';
|
|
18
19
|
|
|
19
20
|
const isWindows = process.platform === 'win32';
|
|
20
21
|
|
|
@@ -51,6 +52,7 @@ async function getNginxStatus(nginxDir) {
|
|
|
51
52
|
// ─── testConfig ───────────────────────────────────────────────────────────────
|
|
52
53
|
|
|
53
54
|
async function testConfig(nginxExe, nginxDir) {
|
|
55
|
+
await ensureNginxInclude(nginxDir);
|
|
54
56
|
const cmd = isWindows ? `& "${nginxExe}" -t` : 'nginx -t';
|
|
55
57
|
const result = await run(cmd, { cwd: nginxDir });
|
|
56
58
|
return {
|
|
@@ -242,25 +242,68 @@ async function renewExpiring(certs) {
|
|
|
242
242
|
// ─── installCertbot ───────────────────────────────────────────────────────────
|
|
243
243
|
|
|
244
244
|
async function installCertbot() {
|
|
245
|
-
if (isWindows) {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
245
|
+
if (!isWindows) {
|
|
246
|
+
const exitCode = await runLive('sudo apt-get install -y certbot', { timeout: 180000 });
|
|
247
|
+
return { success: exitCode === 0 };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── Windows: winget → Chocolatey → direct EFF installer ──────────────────────
|
|
251
|
+
|
|
252
|
+
// 1. Try winget (Windows 10/11 desktop; not on Windows Server by default)
|
|
253
|
+
const wingetCheck = await run('where.exe winget 2>$null');
|
|
254
|
+
if (wingetCheck.success && wingetCheck.stdout.trim().length > 0) {
|
|
250
255
|
console.log(chalk.gray('\n Installing via winget (EFF.Certbot) ...\n'));
|
|
251
256
|
const exitCode = await runLive(
|
|
252
257
|
'winget install -e --id EFF.Certbot --accept-package-agreements --accept-source-agreements',
|
|
253
258
|
{ timeout: 180000 },
|
|
254
259
|
);
|
|
255
|
-
if (exitCode
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
+
if (exitCode === 0) {
|
|
261
|
+
const check = await run(`Test-Path "${CERTBOT_WIN_EXE}"`);
|
|
262
|
+
return { success: check.stdout.trim().toLowerCase() === 'true' };
|
|
263
|
+
}
|
|
264
|
+
console.log(chalk.yellow(' winget failed, trying Chocolatey...\n'));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 2. Try Chocolatey (common on Windows Server)
|
|
268
|
+
const chocoCheck = await run('where.exe choco 2>$null');
|
|
269
|
+
if (chocoCheck.success && chocoCheck.stdout.trim().length > 0) {
|
|
270
|
+
console.log(chalk.gray('\n Installing via Chocolatey ...\n'));
|
|
271
|
+
const exitCode = await runLive('choco install certbot -y', { timeout: 180000 });
|
|
272
|
+
if (exitCode === 0) {
|
|
273
|
+
const check = await run(`Test-Path "${CERTBOT_WIN_EXE}"`);
|
|
274
|
+
return { success: check.stdout.trim().toLowerCase() === 'true' };
|
|
275
|
+
}
|
|
276
|
+
console.log(chalk.yellow(' Chocolatey failed, trying direct download...\n'));
|
|
260
277
|
}
|
|
261
278
|
|
|
262
|
-
|
|
263
|
-
|
|
279
|
+
// 3. Direct download — official EFF installer (NSIS, supports /S silent flag)
|
|
280
|
+
const installerUrl = 'https://dl.eff.org/certbot-beta-installer-win_amd64_signed.exe';
|
|
281
|
+
|
|
282
|
+
console.log(chalk.gray('\n Downloading certbot installer from dl.eff.org ...\n'));
|
|
283
|
+
|
|
284
|
+
const downloadResult = await run(
|
|
285
|
+
`$ProgressPreference='SilentlyContinue'; Invoke-WebRequest -Uri '${installerUrl}' -OutFile "$env:TEMP\\certbot-installer.exe" -UseBasicParsing -TimeoutSec 120`,
|
|
286
|
+
{ timeout: 130000 },
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
if (!downloadResult.success) {
|
|
290
|
+
console.log(chalk.red(' Download failed: ' + (downloadResult.stderr || downloadResult.stdout)));
|
|
291
|
+
return { success: false };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
console.log(chalk.gray(' Running installer silently ...\n'));
|
|
295
|
+
|
|
296
|
+
await run(
|
|
297
|
+
`Start-Process -FilePath "$env:TEMP\\certbot-installer.exe" -ArgumentList '/S' -Wait -NoNewWindow`,
|
|
298
|
+
{ timeout: 120000 },
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
// Cleanup installer
|
|
302
|
+
await run(`Remove-Item -Force "$env:TEMP\\certbot-installer.exe" -ErrorAction SilentlyContinue`);
|
|
303
|
+
|
|
304
|
+
// Verify
|
|
305
|
+
const check = await run(`Test-Path "${CERTBOT_WIN_EXE}"`);
|
|
306
|
+
return { success: check.stdout.trim().toLowerCase() === 'true' };
|
|
264
307
|
}
|
|
265
308
|
|
|
266
309
|
// ─── showSslManager ───────────────────────────────────────────────────────────
|
|
@@ -11,6 +11,63 @@ import fs from 'fs/promises';
|
|
|
11
11
|
import path from 'path';
|
|
12
12
|
import { loadConfig } from './config.js';
|
|
13
13
|
|
|
14
|
+
const isWindows = process.platform === 'win32';
|
|
15
|
+
|
|
16
|
+
/** Returns the conf.d directory for domain config files. */
|
|
17
|
+
function getConfDDir(nginxDir) {
|
|
18
|
+
return isWindows
|
|
19
|
+
? path.join(nginxDir, 'conf', 'conf.d')
|
|
20
|
+
: path.join(nginxDir, 'conf.d');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Returns the path to nginx.conf. */
|
|
24
|
+
function getNginxConfPath(nginxDir) {
|
|
25
|
+
return isWindows
|
|
26
|
+
? path.join(nginxDir, 'conf', 'nginx.conf')
|
|
27
|
+
: path.join(nginxDir, 'nginx.conf');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Returns the include directive line for this platform. */
|
|
31
|
+
function buildIncludeLine(nginxDir) {
|
|
32
|
+
if (isWindows) {
|
|
33
|
+
const fwd = nginxDir.replace(/\\/g, '/');
|
|
34
|
+
return ` include "${fwd}/conf/conf.d/*.conf";`;
|
|
35
|
+
}
|
|
36
|
+
return ` include ${nginxDir}/conf.d/*.conf;`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Ensures that nginx.conf contains an include directive for conf.d/*.conf.
|
|
41
|
+
* If the line is missing it is inserted just before the closing } of the http block.
|
|
42
|
+
* @param {string} nginxDir
|
|
43
|
+
*/
|
|
44
|
+
export async function ensureNginxInclude(nginxDir) {
|
|
45
|
+
const confPath = getNginxConfPath(nginxDir);
|
|
46
|
+
|
|
47
|
+
let content;
|
|
48
|
+
try {
|
|
49
|
+
content = await fs.readFile(confPath, 'utf8');
|
|
50
|
+
} catch {
|
|
51
|
+
return; // nginx.conf not present yet — skip silently
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Already has a conf.d include
|
|
55
|
+
if (/include\s+[^\n]*conf\.d[^\n]*\*\.conf/.test(content)) return;
|
|
56
|
+
|
|
57
|
+
const includeLine = buildIncludeLine(nginxDir);
|
|
58
|
+
|
|
59
|
+
// Insert before the last } in the file (closes the http block)
|
|
60
|
+
const lastBrace = content.lastIndexOf('}');
|
|
61
|
+
if (lastBrace === -1) return;
|
|
62
|
+
|
|
63
|
+
const newContent =
|
|
64
|
+
content.slice(0, lastBrace) +
|
|
65
|
+
`${includeLine}\n` +
|
|
66
|
+
content.slice(lastBrace);
|
|
67
|
+
|
|
68
|
+
await fs.writeFile(confPath, newContent, 'utf8');
|
|
69
|
+
}
|
|
70
|
+
|
|
14
71
|
// ─── DOMAIN DEFAULTS (v2 schema) ─────────────────────────────────────────────
|
|
15
72
|
|
|
16
73
|
export const DOMAIN_DEFAULTS = {
|
|
@@ -274,13 +331,17 @@ export function buildConf(domain, nginxDir, certbotDir) {
|
|
|
274
331
|
*/
|
|
275
332
|
export async function generateConf(domain) {
|
|
276
333
|
const { nginxDir, certbotDir } = loadConfig();
|
|
277
|
-
const
|
|
334
|
+
const confDir = getConfDDir(nginxDir);
|
|
335
|
+
const confPath = path.join(confDir, `${domain.name}.conf`);
|
|
278
336
|
const confContent = buildConf(domain, nginxDir, certbotDir);
|
|
279
337
|
|
|
280
338
|
// Ensure conf.d directory exists
|
|
281
|
-
await fs.mkdir(
|
|
339
|
+
await fs.mkdir(confDir, { recursive: true });
|
|
282
340
|
await fs.writeFile(confPath, confContent, 'utf8');
|
|
283
341
|
|
|
342
|
+
// Ensure nginx.conf includes conf.d
|
|
343
|
+
await ensureNginxInclude(nginxDir);
|
|
344
|
+
|
|
284
345
|
// Update domain with config file path
|
|
285
346
|
domain.configFile = confPath;
|
|
286
347
|
domain.updatedAt = new Date().toISOString();
|
|
@@ -2,6 +2,7 @@ import fs from 'fs/promises';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { run } from '../../core/shell.js';
|
|
4
4
|
import { loadConfig } from '../../core/config.js';
|
|
5
|
+
import { ensureNginxInclude } from '../../core/nginx-conf-generator.js';
|
|
5
6
|
|
|
6
7
|
// ─── Error Types ──────────────────────────────────────────────────────────────
|
|
7
8
|
|
|
@@ -23,6 +24,12 @@ function getNginxDir() {
|
|
|
23
24
|
return nginxDir;
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
function getConfDDir(nginxDir) {
|
|
28
|
+
return process.platform === 'win32'
|
|
29
|
+
? path.join(nginxDir, 'conf', 'conf.d')
|
|
30
|
+
: path.join(nginxDir, 'conf.d');
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
/**
|
|
27
34
|
* Returns the PS-safe invocation string for the nginx binary.
|
|
28
35
|
* On Windows: checks PATH first, then falls back to configured nginxDir.
|
|
@@ -50,7 +57,7 @@ export function validateFilename(filename) {
|
|
|
50
57
|
throw new InvalidFilenameError('Invalid filename');
|
|
51
58
|
}
|
|
52
59
|
const nginxDir = getNginxDir();
|
|
53
|
-
const confDir =
|
|
60
|
+
const confDir = getConfDDir(nginxDir);
|
|
54
61
|
const resolved = path.resolve(path.join(confDir, filename));
|
|
55
62
|
if (!resolved.startsWith(path.resolve(confDir))) {
|
|
56
63
|
throw new InvalidFilenameError('Invalid filename');
|
|
@@ -135,6 +142,7 @@ export async function start() {
|
|
|
135
142
|
}
|
|
136
143
|
|
|
137
144
|
// Test config before starting
|
|
145
|
+
await ensureNginxInclude(nginxDir);
|
|
138
146
|
const testResult = await run(`${nginxExe} -t`);
|
|
139
147
|
if (!testResult.success) {
|
|
140
148
|
return { success: false, output: combineOutput(testResult) };
|
|
@@ -201,11 +209,13 @@ export async function stop() {
|
|
|
201
209
|
|
|
202
210
|
export async function test() {
|
|
203
211
|
const nginxExe = getNginxExe();
|
|
212
|
+
const nginxDir = getNginxDir();
|
|
204
213
|
const versionResult = await run(`${nginxExe} -v`);
|
|
205
214
|
if (!versionResult.success && !versionResult.stderr.includes('nginx/')) {
|
|
206
215
|
throw new NginxNotFoundError('nginx binary not found');
|
|
207
216
|
}
|
|
208
217
|
|
|
218
|
+
await ensureNginxInclude(nginxDir);
|
|
209
219
|
const result = await run(`${nginxExe} -t`);
|
|
210
220
|
return { success: result.success, output: combineOutput(result) };
|
|
211
221
|
}
|
|
@@ -214,7 +224,7 @@ export async function test() {
|
|
|
214
224
|
|
|
215
225
|
export async function listConfigs() {
|
|
216
226
|
const nginxDir = getNginxDir();
|
|
217
|
-
const confDir =
|
|
227
|
+
const confDir = getConfDDir(nginxDir);
|
|
218
228
|
let entries;
|
|
219
229
|
try {
|
|
220
230
|
entries = await fs.readdir(confDir);
|
|
@@ -228,7 +238,7 @@ export async function listConfigs() {
|
|
|
228
238
|
export async function getConfig(filename) {
|
|
229
239
|
validateFilename(filename);
|
|
230
240
|
const nginxDir = getNginxDir();
|
|
231
|
-
const confPath = path.join(nginxDir,
|
|
241
|
+
const confPath = path.join(getConfDDir(nginxDir), filename);
|
|
232
242
|
const content = await fs.readFile(confPath, 'utf8');
|
|
233
243
|
return { content };
|
|
234
244
|
}
|
|
@@ -236,7 +246,7 @@ export async function getConfig(filename) {
|
|
|
236
246
|
export async function saveConfig(filename, content) {
|
|
237
247
|
validateFilename(filename);
|
|
238
248
|
const nginxDir = getNginxDir();
|
|
239
|
-
const confPath = path.join(nginxDir,
|
|
249
|
+
const confPath = path.join(getConfDDir(nginxDir), filename);
|
|
240
250
|
const backupPath = confPath + '.bak';
|
|
241
251
|
|
|
242
252
|
// Backup only if the file already exists (it may be a new file)
|
|
@@ -252,6 +262,7 @@ export async function saveConfig(filename, content) {
|
|
|
252
262
|
await fs.writeFile(confPath, content, 'utf8');
|
|
253
263
|
|
|
254
264
|
const nginxExe = getNginxExe();
|
|
265
|
+
await ensureNginxInclude(nginxDir);
|
|
255
266
|
const result = await run(`${nginxExe} -t`);
|
|
256
267
|
if (!result.success) {
|
|
257
268
|
if (hasBackup) {
|
package/package.json
CHANGED