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,282 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { run } from '../../core/shell.js';
|
|
4
|
+
import { loadConfig } from '../../core/config.js';
|
|
5
|
+
|
|
6
|
+
// ─── Error Types ──────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
export class NginxNotFoundError extends Error { }
|
|
9
|
+
|
|
10
|
+
export class NginxConfigError extends Error {
|
|
11
|
+
constructor(output) {
|
|
12
|
+
super(output);
|
|
13
|
+
this.output = output;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class InvalidFilenameError extends Error { }
|
|
18
|
+
|
|
19
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
function getNginxDir() {
|
|
22
|
+
const { nginxDir } = loadConfig();
|
|
23
|
+
return nginxDir;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Returns the PS-safe invocation string for the nginx binary.
|
|
28
|
+
* On Windows: checks PATH first, then falls back to configured nginxDir.
|
|
29
|
+
* On Linux: always returns 'nginx'.
|
|
30
|
+
*/
|
|
31
|
+
function getNginxExe() {
|
|
32
|
+
const nginxDir = getNginxDir();
|
|
33
|
+
if (process.platform === 'win32') {
|
|
34
|
+
const exe = path.join(nginxDir, 'nginx.exe');
|
|
35
|
+
return `& "${exe}"`;
|
|
36
|
+
}
|
|
37
|
+
return 'nginx';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function combineOutput(result) {
|
|
41
|
+
return [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function validateFilename(filename) {
|
|
45
|
+
if (
|
|
46
|
+
filename.includes('..') ||
|
|
47
|
+
filename.startsWith('/') ||
|
|
48
|
+
/^[A-Za-z]:[/\\]/.test(filename)
|
|
49
|
+
) {
|
|
50
|
+
throw new InvalidFilenameError('Invalid filename');
|
|
51
|
+
}
|
|
52
|
+
const nginxDir = getNginxDir();
|
|
53
|
+
const confDir = path.join(nginxDir, 'conf.d');
|
|
54
|
+
const resolved = path.resolve(path.join(confDir, filename));
|
|
55
|
+
if (!resolved.startsWith(path.resolve(confDir))) {
|
|
56
|
+
throw new InvalidFilenameError('Invalid filename');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── US1: Status ──────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
export async function getStatus() {
|
|
63
|
+
const nginxExe = getNginxExe();
|
|
64
|
+
const versionResult = await run(`${nginxExe} -v`);
|
|
65
|
+
|
|
66
|
+
if (!versionResult.success && !versionResult.stderr.includes('nginx/')) {
|
|
67
|
+
throw new NginxNotFoundError('nginx binary not found');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const versionMatch = versionResult.stderr.match(/nginx\/[\d.]+/);
|
|
71
|
+
const version = versionMatch ? versionMatch[0] : null;
|
|
72
|
+
|
|
73
|
+
let running = false;
|
|
74
|
+
let pid = null;
|
|
75
|
+
|
|
76
|
+
if (process.platform === 'win32') {
|
|
77
|
+
const result = await run('tasklist /FI "IMAGENAME eq nginx.exe" /NH');
|
|
78
|
+
const output = combineOutput(result);
|
|
79
|
+
running = output.toLowerCase().includes('nginx.exe');
|
|
80
|
+
if (running) {
|
|
81
|
+
const match = output.match(/nginx\.exe\s+(\d+)/i);
|
|
82
|
+
pid = match ? parseInt(match[1], 10) : null;
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
const result = await run('pgrep -x nginx');
|
|
86
|
+
running = result.success && result.stdout.trim().length > 0;
|
|
87
|
+
if (running) {
|
|
88
|
+
const firstLine = result.stdout.trim().split('\n')[0];
|
|
89
|
+
pid = firstLine ? parseInt(firstLine, 10) : null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { running, version, pid };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ─── US2: Control ─────────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
export async function reload() {
|
|
99
|
+
const nginxExe = getNginxExe();
|
|
100
|
+
const versionResult = await run(`${nginxExe} -v`);
|
|
101
|
+
if (!versionResult.success && !versionResult.stderr.includes('nginx/')) {
|
|
102
|
+
throw new NginxNotFoundError('nginx binary not found');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const result = process.platform === 'win32'
|
|
106
|
+
? await run(`${nginxExe} -s reload`)
|
|
107
|
+
: await run('nginx -s reload');
|
|
108
|
+
return { success: result.success, output: combineOutput(result) };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function restart() {
|
|
112
|
+
const nginxExe = getNginxExe();
|
|
113
|
+
const versionResult = await run(`${nginxExe} -v`);
|
|
114
|
+
if (!versionResult.success && !versionResult.stderr.includes('nginx/')) {
|
|
115
|
+
throw new NginxNotFoundError('nginx binary not found');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const stopResult = process.platform === 'win32'
|
|
119
|
+
? await run('taskkill /f /IM nginx.exe')
|
|
120
|
+
: await run('nginx -s stop');
|
|
121
|
+
const startResult = await run(nginxExe);
|
|
122
|
+
const output = [combineOutput(stopResult), combineOutput(startResult)]
|
|
123
|
+
.filter(Boolean)
|
|
124
|
+
.join('\n')
|
|
125
|
+
.trim();
|
|
126
|
+
return { success: startResult.success, output };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function start() {
|
|
130
|
+
const nginxExe = getNginxExe();
|
|
131
|
+
const nginxDir = getNginxDir();
|
|
132
|
+
const versionResult = await run(`${nginxExe} -v`);
|
|
133
|
+
if (!versionResult.success && !versionResult.stderr.includes('nginx/')) {
|
|
134
|
+
throw new NginxNotFoundError('nginx binary not found');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Test config before starting
|
|
138
|
+
const testResult = await run(`${nginxExe} -t`);
|
|
139
|
+
if (!testResult.success) {
|
|
140
|
+
return { success: false, output: combineOutput(testResult) };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Start nginx
|
|
144
|
+
if (process.platform === 'win32') {
|
|
145
|
+
// On Windows, nginx runs in foreground so we need Start-Process
|
|
146
|
+
const startCmd = `Start-Process -FilePath "${path.join(nginxDir, 'nginx.exe')}" -WorkingDirectory "${nginxDir}" -WindowStyle Hidden`;
|
|
147
|
+
await run(startCmd, { timeout: 10000 });
|
|
148
|
+
} else {
|
|
149
|
+
const result = await run('nginx', { cwd: nginxDir, timeout: 15000 });
|
|
150
|
+
if (!result.success) {
|
|
151
|
+
return { success: false, output: combineOutput(result) };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Wait briefly then verify nginx is running
|
|
156
|
+
await new Promise(r => setTimeout(r, 1500));
|
|
157
|
+
|
|
158
|
+
let running = false;
|
|
159
|
+
if (process.platform === 'win32') {
|
|
160
|
+
const check = await run('tasklist /FI "IMAGENAME eq nginx.exe" /NH');
|
|
161
|
+
running = check.success && check.stdout.toLowerCase().includes('nginx.exe');
|
|
162
|
+
} else {
|
|
163
|
+
const check = await run('pgrep -x nginx');
|
|
164
|
+
running = check.exitCode === 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (running) {
|
|
168
|
+
return { success: true, output: 'nginx started successfully' };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Didn't start - try to get error log
|
|
172
|
+
const logPath = process.platform === 'win32'
|
|
173
|
+
? path.join(nginxDir, 'logs', 'error.log')
|
|
174
|
+
: '/var/log/nginx/error.log';
|
|
175
|
+
const logCmd = process.platform === 'win32'
|
|
176
|
+
? `Get-Content -Tail 20 "${logPath}" -ErrorAction SilentlyContinue`
|
|
177
|
+
: `tail -n 20 "${logPath}" 2>/dev/null`;
|
|
178
|
+
const logResult = await run(logCmd);
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
success: false,
|
|
182
|
+
output: 'nginx did not start\n\n' + (logResult.success ? logResult.stdout : '(no error log)'),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export async function stop() {
|
|
187
|
+
const nginxExe = getNginxExe();
|
|
188
|
+
const versionResult = await run(`${nginxExe} -v`);
|
|
189
|
+
if (!versionResult.success && !versionResult.stderr.includes('nginx/')) {
|
|
190
|
+
throw new NginxNotFoundError('nginx binary not found');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const result = process.platform === 'win32'
|
|
194
|
+
? await run('taskkill /f /IM nginx.exe')
|
|
195
|
+
: await run('nginx -s stop');
|
|
196
|
+
|
|
197
|
+
return { success: result.success, output: combineOutput(result) || 'nginx stopped' };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ─── US3: Test Config ─────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
export async function test() {
|
|
203
|
+
const nginxExe = getNginxExe();
|
|
204
|
+
const versionResult = await run(`${nginxExe} -v`);
|
|
205
|
+
if (!versionResult.success && !versionResult.stderr.includes('nginx/')) {
|
|
206
|
+
throw new NginxNotFoundError('nginx binary not found');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const result = await run(`${nginxExe} -t`);
|
|
210
|
+
return { success: result.success, output: combineOutput(result) };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ─── US4: Config File Management ──────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
export async function listConfigs() {
|
|
216
|
+
const nginxDir = getNginxDir();
|
|
217
|
+
const confDir = path.join(nginxDir, 'conf.d');
|
|
218
|
+
let entries;
|
|
219
|
+
try {
|
|
220
|
+
entries = await fs.readdir(confDir);
|
|
221
|
+
} catch (err) {
|
|
222
|
+
if (err.code === 'ENOENT') return [];
|
|
223
|
+
throw err;
|
|
224
|
+
}
|
|
225
|
+
return entries.filter(f => f.endsWith('.conf'));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function getConfig(filename) {
|
|
229
|
+
validateFilename(filename);
|
|
230
|
+
const nginxDir = getNginxDir();
|
|
231
|
+
const confPath = path.join(nginxDir, 'conf.d', filename);
|
|
232
|
+
const content = await fs.readFile(confPath, 'utf8');
|
|
233
|
+
return { content };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function saveConfig(filename, content) {
|
|
237
|
+
validateFilename(filename);
|
|
238
|
+
const nginxDir = getNginxDir();
|
|
239
|
+
const confPath = path.join(nginxDir, 'conf.d', filename);
|
|
240
|
+
const backupPath = confPath + '.bak';
|
|
241
|
+
|
|
242
|
+
// Backup only if the file already exists (it may be a new file)
|
|
243
|
+
let hasBackup = false;
|
|
244
|
+
try {
|
|
245
|
+
await fs.copyFile(confPath, backupPath);
|
|
246
|
+
hasBackup = true;
|
|
247
|
+
} catch (err) {
|
|
248
|
+
if (err.code !== 'ENOENT') throw err;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
await fs.mkdir(path.dirname(confPath), { recursive: true });
|
|
252
|
+
await fs.writeFile(confPath, content, 'utf8');
|
|
253
|
+
|
|
254
|
+
const nginxExe = getNginxExe();
|
|
255
|
+
const result = await run(`${nginxExe} -t`);
|
|
256
|
+
if (!result.success) {
|
|
257
|
+
if (hasBackup) {
|
|
258
|
+
await fs.copyFile(backupPath, confPath);
|
|
259
|
+
} else {
|
|
260
|
+
try { await fs.unlink(confPath); } catch { /* ignore */ }
|
|
261
|
+
}
|
|
262
|
+
throw new NginxConfigError(combineOutput(result));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return { success: true, output: combineOutput(result) };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ─── US5: Logs ────────────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
export async function getLogs(lines = 100) {
|
|
271
|
+
const nginxDir = getNginxDir();
|
|
272
|
+
const logPath = path.join(nginxDir, 'logs', 'error.log');
|
|
273
|
+
let content;
|
|
274
|
+
try {
|
|
275
|
+
content = await fs.readFile(logPath, 'utf8');
|
|
276
|
+
} catch (err) {
|
|
277
|
+
if (err.code === 'ENOENT') return { lines: [] };
|
|
278
|
+
throw err;
|
|
279
|
+
}
|
|
280
|
+
const allLines = content.split('\n').filter(Boolean);
|
|
281
|
+
return { lines: allLines.slice(-lines) };
|
|
282
|
+
}
|