easy-devops 0.1.0
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/LICENSE +21 -0
- package/README.md +325 -0
- package/cli/index.js +91 -0
- package/cli/managers/domain-manager.js +451 -0
- package/cli/managers/nginx-manager.js +329 -0
- package/cli/managers/node-manager.js +275 -0
- package/cli/managers/ssl-manager.js +397 -0
- package/cli/menus/.gitkeep +0 -0
- package/cli/menus/dashboard.js +223 -0
- package/cli/menus/domains.js +5 -0
- package/cli/menus/nginx.js +5 -0
- package/cli/menus/nodejs.js +5 -0
- package/cli/menus/settings.js +83 -0
- package/cli/menus/ssl.js +5 -0
- package/core/config.js +37 -0
- package/core/db.js +30 -0
- package/core/detector.js +257 -0
- package/core/nginx-conf-generator.js +309 -0
- package/core/shell.js +151 -0
- package/dashboard/lib/.gitkeep +0 -0
- package/dashboard/lib/cert-reader.js +59 -0
- package/dashboard/lib/domains-db.js +51 -0
- package/dashboard/lib/nginx-conf-generator.js +16 -0
- package/dashboard/lib/nginx-service.js +282 -0
- package/dashboard/public/js/app.js +486 -0
- package/dashboard/routes/.gitkeep +0 -0
- package/dashboard/routes/auth.js +30 -0
- package/dashboard/routes/domains.js +300 -0
- package/dashboard/routes/nginx.js +151 -0
- package/dashboard/routes/settings.js +78 -0
- package/dashboard/routes/ssl.js +105 -0
- package/dashboard/server.js +79 -0
- package/dashboard/views/index.ejs +327 -0
- package/dashboard/views/partials/domain-form.ejs +229 -0
- package/dashboard/views/partials/domains-panel.ejs +66 -0
- package/dashboard/views/partials/login.ejs +50 -0
- package/dashboard/views/partials/nginx-panel.ejs +90 -0
- package/dashboard/views/partials/overview.ejs +67 -0
- package/dashboard/views/partials/settings-panel.ejs +37 -0
- package/dashboard/views/partials/sidebar.ejs +45 -0
- package/dashboard/views/partials/ssl-panel.ejs +53 -0
- package/data/.gitkeep +0 -0
- package/install.bat +41 -0
- package/install.ps1 +653 -0
- package/install.sh +452 -0
- package/lib/installer/.gitkeep +0 -0
- package/lib/installer/detect.sh +88 -0
- package/lib/installer/node-versions.sh +109 -0
- package/lib/installer/nvm-bootstrap.sh +77 -0
- package/lib/installer/picker.sh +163 -0
- package/lib/installer/progress.sh +25 -0
- package/package.json +67 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { loadConfig, saveConfig } from '../../core/config.js';
|
|
4
|
+
import { dbGet } from '../../core/db.js';
|
|
5
|
+
|
|
6
|
+
export default async function settingsMenu() {
|
|
7
|
+
// T021: detect missing/corrupted config before loadConfig() creates defaults
|
|
8
|
+
const wasStored = dbGet('config') !== undefined;
|
|
9
|
+
const config = loadConfig();
|
|
10
|
+
|
|
11
|
+
if (!wasStored) {
|
|
12
|
+
console.log(chalk.yellow('\nDefaults applied — config not found or reset.'));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
while (true) {
|
|
16
|
+
// T013: display current values
|
|
17
|
+
const passwordDisplay = config.dashboardPassword ? '***' : '(not set)';
|
|
18
|
+
|
|
19
|
+
console.log();
|
|
20
|
+
console.log(chalk.bold('⚙️ Settings'));
|
|
21
|
+
console.log(chalk.gray('─'.repeat(40)));
|
|
22
|
+
console.log(` Dashboard Port: ${chalk.cyan(config.dashboardPort)}`);
|
|
23
|
+
console.log(` Dashboard Password: ${chalk.cyan(passwordDisplay)}`);
|
|
24
|
+
console.log(` Nginx Directory: ${chalk.cyan(config.nginxDir)}`);
|
|
25
|
+
console.log(` Certbot Directory: ${chalk.cyan(config.certbotDir)}`);
|
|
26
|
+
console.log();
|
|
27
|
+
|
|
28
|
+
// T014: field selection
|
|
29
|
+
const { field } = await inquirer.prompt([{
|
|
30
|
+
type: 'list',
|
|
31
|
+
name: 'field',
|
|
32
|
+
message: 'Select a field to edit:',
|
|
33
|
+
choices: [
|
|
34
|
+
{ name: `Dashboard Port (${config.dashboardPort})`, value: 'dashboardPort' },
|
|
35
|
+
{ name: `Dashboard Password (${passwordDisplay})`, value: 'dashboardPassword' },
|
|
36
|
+
{ name: `Nginx Directory (${config.nginxDir})`, value: 'nginxDir' },
|
|
37
|
+
{ name: `Certbot Directory (${config.certbotDir})`, value: 'certbotDir' },
|
|
38
|
+
{ name: '← Back', value: 'back' },
|
|
39
|
+
],
|
|
40
|
+
}]);
|
|
41
|
+
|
|
42
|
+
if (field === 'back') return;
|
|
43
|
+
|
|
44
|
+
if (field === 'dashboardPort') {
|
|
45
|
+
// T015: port validation with inline error + re-prompt
|
|
46
|
+
const { value } = await inquirer.prompt([{
|
|
47
|
+
type: 'input',
|
|
48
|
+
name: 'value',
|
|
49
|
+
message: 'Dashboard port (1–65535):',
|
|
50
|
+
default: String(config.dashboardPort),
|
|
51
|
+
validate(input) {
|
|
52
|
+
const port = parseInt(input, 10);
|
|
53
|
+
if (isNaN(port) || !Number.isInteger(port) || port < 1 || port > 65535) {
|
|
54
|
+
return chalk.red('Must be an integer between 1 and 65535.');
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
},
|
|
58
|
+
}]);
|
|
59
|
+
config.dashboardPort = parseInt(value, 10);
|
|
60
|
+
saveConfig(config); // T016
|
|
61
|
+
|
|
62
|
+
} else if (field === 'dashboardPassword') {
|
|
63
|
+
const { value } = await inquirer.prompt([{
|
|
64
|
+
type: 'password',
|
|
65
|
+
name: 'value',
|
|
66
|
+
message: 'New dashboard password (leave blank to clear):',
|
|
67
|
+
mask: '*',
|
|
68
|
+
}]);
|
|
69
|
+
config.dashboardPassword = value;
|
|
70
|
+
saveConfig(config); // T016
|
|
71
|
+
|
|
72
|
+
} else {
|
|
73
|
+
const { value } = await inquirer.prompt([{
|
|
74
|
+
type: 'input',
|
|
75
|
+
name: 'value',
|
|
76
|
+
message: `New value for ${field}:`,
|
|
77
|
+
default: config[field],
|
|
78
|
+
}]);
|
|
79
|
+
config[field] = value;
|
|
80
|
+
saveConfig(config); // T016
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
package/cli/menus/ssl.js
ADDED
package/core/config.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import { dbGet, dbSet } from './db.js';
|
|
3
|
+
|
|
4
|
+
const platform = os.platform();
|
|
5
|
+
|
|
6
|
+
const DEFAULTS = {
|
|
7
|
+
linux: {
|
|
8
|
+
nginxDir: '/etc/nginx',
|
|
9
|
+
certbotDir: '/etc/letsencrypt',
|
|
10
|
+
dashboardPort: 6443,
|
|
11
|
+
dashboardPassword: '',
|
|
12
|
+
os: 'linux',
|
|
13
|
+
},
|
|
14
|
+
win32: {
|
|
15
|
+
nginxDir: 'C:\\nginx',
|
|
16
|
+
certbotDir: 'C:\\certbot',
|
|
17
|
+
dashboardPort: 6443,
|
|
18
|
+
dashboardPassword: '',
|
|
19
|
+
os: 'win32',
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const defaultConfig = DEFAULTS[platform] ?? DEFAULTS.linux;
|
|
24
|
+
|
|
25
|
+
export function loadConfig() {
|
|
26
|
+
const stored = dbGet('config');
|
|
27
|
+
if (stored) {
|
|
28
|
+
return { ...defaultConfig, ...stored };
|
|
29
|
+
}
|
|
30
|
+
const config = { ...defaultConfig };
|
|
31
|
+
saveConfig(config);
|
|
32
|
+
return config;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function saveConfig(config) {
|
|
36
|
+
dbSet('config', config);
|
|
37
|
+
}
|
package/core/db.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { GoodDB, SQLiteDriver } from 'good.db';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const DATA_DIR = path.join(__dirname, '..', 'data');
|
|
8
|
+
const DB_PATH = path.join(DATA_DIR, 'easy-devops.sqlite');
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
12
|
+
} catch (err) {
|
|
13
|
+
process.stderr.write(`[ERROR] Failed to create database directory: ${DATA_DIR}\n`);
|
|
14
|
+
process.stderr.write(`Hint: Check that the current user has write access to: ${path.dirname(DATA_DIR)}\n`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let db;
|
|
19
|
+
try {
|
|
20
|
+
db = new GoodDB(new SQLiteDriver({ path: DB_PATH }));
|
|
21
|
+
} catch (err) {
|
|
22
|
+
process.stderr.write(`[ERROR] Failed to initialize database at: ${DB_PATH}\n`);
|
|
23
|
+
process.stderr.write(`Hint: Check that the current user has write access to: ${DATA_DIR}\n`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export { db };
|
|
28
|
+
export const dbGet = (key) => db.get(key);
|
|
29
|
+
export const dbSet = (key, value) => db.set(key, value);
|
|
30
|
+
export const dbDelete = (key) => db.delete(key);
|
package/core/detector.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/detector.js
|
|
3
|
+
*
|
|
4
|
+
* Automatic server environment detection.
|
|
5
|
+
*
|
|
6
|
+
* DB key: 'system-detection'
|
|
7
|
+
*
|
|
8
|
+
* Exported functions:
|
|
9
|
+
* - runDetection() — collects all system info, writes to DB, silent on success
|
|
10
|
+
* - showSystemStatus() — reads DB, prints chalk-formatted table to stdout
|
|
11
|
+
* - getDetectionResult() — thin wrapper around dbGet('system-detection') for other modules
|
|
12
|
+
*
|
|
13
|
+
* SystemDetectionResult schema:
|
|
14
|
+
* {
|
|
15
|
+
* detectedAt: string, // ISO 8601 timestamp
|
|
16
|
+
* os: { type, release },
|
|
17
|
+
* nodejs: { version },
|
|
18
|
+
* npm: { installed, version },
|
|
19
|
+
* nginx: { installed, version, path },
|
|
20
|
+
* certbot: { installed, version }
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* Returns undefined (never throws) when detection has never run.
|
|
24
|
+
* Other modules should import getDetectionResult() rather than coupling to the raw DB key.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import os from 'os';
|
|
28
|
+
import chalk from 'chalk';
|
|
29
|
+
import { dbGet, dbSet } from './db.js';
|
|
30
|
+
import { run } from './shell.js';
|
|
31
|
+
import { loadConfig } from './config.js';
|
|
32
|
+
|
|
33
|
+
// ─── runDetection ─────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
export async function runDetection() {
|
|
36
|
+
// OS — no subprocess needed
|
|
37
|
+
const platform = os.platform();
|
|
38
|
+
const osInfo = {
|
|
39
|
+
type: platform === 'linux' || platform === 'win32' ? platform : 'unknown',
|
|
40
|
+
release: os.release(),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Node.js — always available
|
|
44
|
+
const nodejsInfo = { version: process.version };
|
|
45
|
+
|
|
46
|
+
// npm
|
|
47
|
+
const npmResult = await run('npm --version');
|
|
48
|
+
const npmInfo = npmResult.success
|
|
49
|
+
? { installed: true, version: npmResult.stdout }
|
|
50
|
+
: { installed: false, version: null };
|
|
51
|
+
|
|
52
|
+
// nginx detection — two-step on Windows: PATH first, configured path as fallback.
|
|
53
|
+
// Detection intentionally does NOT rely on exit code because nginx -v writes to
|
|
54
|
+
// stderr and may exit non-zero on some Windows builds. Instead we look for the
|
|
55
|
+
// version string in combined output, matching the same approach as nginx-manager.
|
|
56
|
+
const { nginxDir } = loadConfig();
|
|
57
|
+
|
|
58
|
+
let nginxInfo;
|
|
59
|
+
|
|
60
|
+
if (platform === 'win32') {
|
|
61
|
+
let detectedPath = null;
|
|
62
|
+
let combined = '';
|
|
63
|
+
|
|
64
|
+
// Step 1: PATH check
|
|
65
|
+
// Use where.exe — plain `where` in PowerShell is an alias for Where-Object
|
|
66
|
+
// and always exits 0, giving a false positive.
|
|
67
|
+
const whereResult = await run('where.exe nginx');
|
|
68
|
+
if (whereResult.success && whereResult.stdout) {
|
|
69
|
+
detectedPath = whereResult.stdout.split('\n')[0].trim();
|
|
70
|
+
const r = await run('nginx -v');
|
|
71
|
+
combined = r.stdout + r.stderr;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Step 2: configured path fallback (if PATH check found nothing)
|
|
75
|
+
if (!combined.includes('nginx/')) {
|
|
76
|
+
const configuredExe = `${nginxDir}\\nginx.exe`;
|
|
77
|
+
const r = await run(`& "${configuredExe}" -v`);
|
|
78
|
+
combined = r.stdout + r.stderr;
|
|
79
|
+
if (combined.includes('nginx/')) {
|
|
80
|
+
detectedPath = configuredExe;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const versionMatch = combined.match(/nginx\/([^\s]+)/);
|
|
85
|
+
nginxInfo = versionMatch
|
|
86
|
+
? { installed: true, version: versionMatch[1], path: detectedPath }
|
|
87
|
+
: { installed: false, version: null, path: null };
|
|
88
|
+
} else {
|
|
89
|
+
const r = await run('nginx -v 2>&1');
|
|
90
|
+
const combined = r.stdout + r.stderr;
|
|
91
|
+
const versionMatch = combined.match(/nginx\/([^\s]+)/);
|
|
92
|
+
if (versionMatch) {
|
|
93
|
+
const pathResult = await run('which nginx');
|
|
94
|
+
nginxInfo = {
|
|
95
|
+
installed: true,
|
|
96
|
+
version: versionMatch[1],
|
|
97
|
+
path: pathResult.success ? pathResult.stdout.split('\n')[0].trim() : null,
|
|
98
|
+
};
|
|
99
|
+
} else {
|
|
100
|
+
nginxInfo = { installed: false, version: null, path: null };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// certbot — try PATH first, then the well-known Windows install location.
|
|
105
|
+
// The winget/official installer puts certbot in C:\Program Files\Certbot\bin\
|
|
106
|
+
// which may not be reflected in the current session PATH immediately after install.
|
|
107
|
+
const CERTBOT_WIN_EXE = 'C:\\Program Files\\Certbot\\bin\\certbot.exe';
|
|
108
|
+
let certbotExe = 'certbot';
|
|
109
|
+
|
|
110
|
+
if (platform === 'win32') {
|
|
111
|
+
const pathCheck = await run('where.exe certbot');
|
|
112
|
+
if (pathCheck.exitCode !== 0 || !pathCheck.stdout.trim()) {
|
|
113
|
+
// Fall back to the known install path
|
|
114
|
+
const exeCheck = await run(`Test-Path "${CERTBOT_WIN_EXE}"`);
|
|
115
|
+
if (exeCheck.stdout.trim().toLowerCase() === 'true') {
|
|
116
|
+
certbotExe = CERTBOT_WIN_EXE;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Use & "..." in PowerShell when the exe is a full path, otherwise bare name
|
|
122
|
+
const certbotCmd = (platform === 'win32' && certbotExe !== 'certbot')
|
|
123
|
+
? `& "${certbotExe}" --version`
|
|
124
|
+
: `${certbotExe} --version`;
|
|
125
|
+
const certbotResult = await run(certbotCmd);
|
|
126
|
+
const certbotCombined = certbotResult.stdout + ' ' + certbotResult.stderr;
|
|
127
|
+
let certbotInfo;
|
|
128
|
+
if (certbotResult.success || certbotCombined.match(/certbot\s+[\d.]+/i)) {
|
|
129
|
+
const match = certbotCombined.match(/certbot\s+([\d.]+)/i);
|
|
130
|
+
certbotInfo = { installed: true, version: match ? match[1] : null };
|
|
131
|
+
} else {
|
|
132
|
+
certbotInfo = { installed: false, version: null };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const result = {
|
|
136
|
+
detectedAt: new Date().toISOString(),
|
|
137
|
+
os: osInfo,
|
|
138
|
+
nodejs: nodejsInfo,
|
|
139
|
+
npm: npmInfo,
|
|
140
|
+
nginx: nginxInfo,
|
|
141
|
+
certbot: certbotInfo,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
dbSet('system-detection', result);
|
|
146
|
+
} catch (err) {
|
|
147
|
+
process.stderr.write(`[ERROR] Failed to persist detection results: ${err.message}\n`);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ─── showSystemStatus ─────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
export function showSystemStatus() {
|
|
155
|
+
const result = dbGet('system-detection');
|
|
156
|
+
|
|
157
|
+
if (!result) {
|
|
158
|
+
console.log(chalk.yellow('No system detection data available.'));
|
|
159
|
+
console.log(chalk.gray('Run the tool to detect your environment automatically.'));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const SEP = chalk.gray('─'.repeat(42));
|
|
164
|
+
const label = (s) => chalk.bold(s.padEnd(14));
|
|
165
|
+
|
|
166
|
+
console.log();
|
|
167
|
+
console.log(chalk.bold.cyan('System Information'));
|
|
168
|
+
console.log(SEP);
|
|
169
|
+
|
|
170
|
+
// OS
|
|
171
|
+
console.log(label('OS') + chalk.white(`${result.os.type} (${result.os.release})`));
|
|
172
|
+
|
|
173
|
+
// Node.js
|
|
174
|
+
console.log(label('Node.js') + chalk.white(result.nodejs.version));
|
|
175
|
+
|
|
176
|
+
// npm
|
|
177
|
+
if (result.npm.installed) {
|
|
178
|
+
console.log(label('npm') + chalk.white(result.npm.version));
|
|
179
|
+
} else {
|
|
180
|
+
console.log(label('npm') + chalk.red('✗ not installed'));
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// nginx
|
|
184
|
+
if (result.nginx.installed) {
|
|
185
|
+
const parts = [chalk.green('✓'), result.nginx.version, result.nginx.path]
|
|
186
|
+
.filter(Boolean)
|
|
187
|
+
.join(' ');
|
|
188
|
+
console.log(label('nginx') + parts);
|
|
189
|
+
} else {
|
|
190
|
+
console.log(label('nginx') + chalk.red('✗ not installed'));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// certbot
|
|
194
|
+
if (result.certbot.installed) {
|
|
195
|
+
const parts = [chalk.green('✓'), result.certbot.version].filter(Boolean).join(' ');
|
|
196
|
+
console.log(label('certbot') + parts);
|
|
197
|
+
} else {
|
|
198
|
+
console.log(label('certbot') + chalk.red('✗ not installed'));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log(SEP);
|
|
202
|
+
console.log(chalk.gray(`Last detected: ${result.detectedAt}`));
|
|
203
|
+
console.log();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ─── getDetectionResult ───────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Returns the latest SystemDetectionResult from the database,
|
|
210
|
+
* or undefined if detection has never run.
|
|
211
|
+
*
|
|
212
|
+
* Usage by other modules:
|
|
213
|
+
* import { getDetectionResult } from '../core/detector.js';
|
|
214
|
+
* const detection = getDetectionResult();
|
|
215
|
+
* const { nginx } = detection ?? {};
|
|
216
|
+
*/
|
|
217
|
+
export function getDetectionResult() {
|
|
218
|
+
return dbGet('system-detection');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ─── formatStatusLine ─────────────────────────────────────────────────────────
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Returns a compact inline status string for embedding in the main menu header.
|
|
225
|
+
* Format: "nginx: ✅ v1.26 | certbot: ✅ | node: v20.11"
|
|
226
|
+
* Returns a warning string if detection result is undefined.
|
|
227
|
+
*/
|
|
228
|
+
export function formatStatusLine() {
|
|
229
|
+
const result = getDetectionResult();
|
|
230
|
+
|
|
231
|
+
if (!result) {
|
|
232
|
+
return chalk.yellow('⚠ System detection not available — run detection first.');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const parts = [];
|
|
236
|
+
|
|
237
|
+
// nginx
|
|
238
|
+
if (result.nginx.installed) {
|
|
239
|
+
const ver = result.nginx.version ? ` v${result.nginx.version}` : '';
|
|
240
|
+
parts.push(`nginx: ✅${ver}`);
|
|
241
|
+
} else {
|
|
242
|
+
parts.push(`nginx: ${chalk.yellow('⚠ not found')}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// certbot
|
|
246
|
+
if (result.certbot.installed) {
|
|
247
|
+
const ver = result.certbot.version ? ` v${result.certbot.version}` : '';
|
|
248
|
+
parts.push(`certbot: ✅${ver}`);
|
|
249
|
+
} else {
|
|
250
|
+
parts.push(`certbot: ${chalk.yellow('⚠ not found')}`);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// node
|
|
254
|
+
parts.push(`node: ${result.nodejs.version}`);
|
|
255
|
+
|
|
256
|
+
return parts.join(' | ');
|
|
257
|
+
}
|