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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +325 -0
  3. package/cli/index.js +91 -0
  4. package/cli/managers/domain-manager.js +451 -0
  5. package/cli/managers/nginx-manager.js +329 -0
  6. package/cli/managers/node-manager.js +275 -0
  7. package/cli/managers/ssl-manager.js +397 -0
  8. package/cli/menus/.gitkeep +0 -0
  9. package/cli/menus/dashboard.js +223 -0
  10. package/cli/menus/domains.js +5 -0
  11. package/cli/menus/nginx.js +5 -0
  12. package/cli/menus/nodejs.js +5 -0
  13. package/cli/menus/settings.js +83 -0
  14. package/cli/menus/ssl.js +5 -0
  15. package/core/config.js +37 -0
  16. package/core/db.js +30 -0
  17. package/core/detector.js +257 -0
  18. package/core/nginx-conf-generator.js +309 -0
  19. package/core/shell.js +151 -0
  20. package/dashboard/lib/.gitkeep +0 -0
  21. package/dashboard/lib/cert-reader.js +59 -0
  22. package/dashboard/lib/domains-db.js +51 -0
  23. package/dashboard/lib/nginx-conf-generator.js +16 -0
  24. package/dashboard/lib/nginx-service.js +282 -0
  25. package/dashboard/public/js/app.js +486 -0
  26. package/dashboard/routes/.gitkeep +0 -0
  27. package/dashboard/routes/auth.js +30 -0
  28. package/dashboard/routes/domains.js +300 -0
  29. package/dashboard/routes/nginx.js +151 -0
  30. package/dashboard/routes/settings.js +78 -0
  31. package/dashboard/routes/ssl.js +105 -0
  32. package/dashboard/server.js +79 -0
  33. package/dashboard/views/index.ejs +327 -0
  34. package/dashboard/views/partials/domain-form.ejs +229 -0
  35. package/dashboard/views/partials/domains-panel.ejs +66 -0
  36. package/dashboard/views/partials/login.ejs +50 -0
  37. package/dashboard/views/partials/nginx-panel.ejs +90 -0
  38. package/dashboard/views/partials/overview.ejs +67 -0
  39. package/dashboard/views/partials/settings-panel.ejs +37 -0
  40. package/dashboard/views/partials/sidebar.ejs +45 -0
  41. package/dashboard/views/partials/ssl-panel.ejs +53 -0
  42. package/data/.gitkeep +0 -0
  43. package/install.bat +41 -0
  44. package/install.ps1 +653 -0
  45. package/install.sh +452 -0
  46. package/lib/installer/.gitkeep +0 -0
  47. package/lib/installer/detect.sh +88 -0
  48. package/lib/installer/node-versions.sh +109 -0
  49. package/lib/installer/nvm-bootstrap.sh +77 -0
  50. package/lib/installer/picker.sh +163 -0
  51. package/lib/installer/progress.sh +25 -0
  52. 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
+ }
@@ -0,0 +1,5 @@
1
+ import { showSslManager } from '../managers/ssl-manager.js';
2
+
3
+ export default async function sslMenu() {
4
+ await showSslManager();
5
+ }
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);
@@ -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
+ }