ezpm2gui 1.4.0 → 1.5.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 (39) hide show
  1. package/README.md +295 -295
  2. package/bin/ezpm2gui.js +8 -8
  3. package/bin/ezpm2gui.ts +51 -51
  4. package/bin/generate-ecosystem.js +35 -35
  5. package/bin/generate-ecosystem.ts +56 -56
  6. package/dist/index.js +1 -1
  7. package/dist/server/config/project-configs.json +236 -0
  8. package/dist/server/index.js +212 -25
  9. package/dist/server/routes/deployApplication.js +6 -5
  10. package/dist/server/routes/remoteConnections.js +260 -0
  11. package/dist/server/routes/updates.d.ts +3 -0
  12. package/dist/server/routes/updates.js +135 -0
  13. package/dist/server/utils/remote-connection.d.ts +18 -0
  14. package/dist/server/utils/remote-connection.js +216 -9
  15. package/package.json +73 -71
  16. package/scripts/postinstall.js +36 -36
  17. package/src/client/build/asset-manifest.json +6 -6
  18. package/src/client/build/favicon.ico +2 -2
  19. package/src/client/build/index.html +1 -1
  20. package/src/client/build/logo192.svg +7 -7
  21. package/src/client/build/logo512.svg +7 -7
  22. package/src/client/build/manifest.json +24 -24
  23. package/src/client/build/static/css/main.2d095544.css +5 -0
  24. package/src/client/build/static/css/main.2d095544.css.map +1 -0
  25. package/src/client/build/static/js/main.17e17668.js +3 -0
  26. package/src/client/build/static/js/main.17e17668.js.map +1 -0
  27. package/dist/server/config/cron-jobs.json +0 -1
  28. package/dist/server/config/remote-connections.json +0 -3
  29. package/dist/server/daemon/ezpm2gui.err.log +0 -414
  30. package/dist/server/daemon/ezpm2gui.exe +0 -0
  31. package/dist/server/daemon/ezpm2gui.exe.config +0 -6
  32. package/dist/server/daemon/ezpm2gui.out.log +0 -289
  33. package/dist/server/daemon/ezpm2gui.wrapper.log +0 -172
  34. package/dist/server/daemon/ezpm2gui.xml +0 -32
  35. package/src/client/build/static/css/main.c506cba5.css +0 -5
  36. package/src/client/build/static/css/main.c506cba5.css.map +0 -1
  37. package/src/client/build/static/js/main.5278cddd.js +0 -3
  38. package/src/client/build/static/js/main.5278cddd.js.map +0 -1
  39. /package/src/client/build/static/js/{main.5278cddd.js.LICENSE.txt → main.17e17668.js.LICENSE.txt} +0 -0
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const express_1 = require("express");
7
+ const child_process_1 = require("child_process");
8
+ const path_1 = __importDefault(require("path"));
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const https_1 = __importDefault(require("https"));
11
+ // @group Utilities : Read the current installed version from package.json
12
+ const getCurrentVersion = () => {
13
+ try {
14
+ // Try the package root (works when run from source or dist)
15
+ const candidates = [
16
+ path_1.default.resolve(__dirname, '../../../package.json'),
17
+ path_1.default.resolve(__dirname, '../../package.json'),
18
+ path_1.default.resolve(process.cwd(), 'package.json'),
19
+ ];
20
+ for (const p of candidates) {
21
+ if (fs_1.default.existsSync(p)) {
22
+ const pkg = JSON.parse(fs_1.default.readFileSync(p, 'utf8'));
23
+ if (pkg.name === 'ezpm2gui')
24
+ return pkg.version;
25
+ }
26
+ }
27
+ }
28
+ catch {
29
+ // fall through
30
+ }
31
+ return '0.0.0';
32
+ };
33
+ // @group Utilities : Fetch latest version info from npm registry (no external deps)
34
+ const fetchNpmLatest = () => new Promise((resolve, reject) => {
35
+ const req = https_1.default.get('https://registry.npmjs.org/ezpm2gui/latest', { headers: { Accept: 'application/json' } }, (res) => {
36
+ let data = '';
37
+ res.on('data', (chunk) => { data += chunk; });
38
+ res.on('end', () => {
39
+ var _a;
40
+ try {
41
+ const json = JSON.parse(data);
42
+ resolve({
43
+ version: json.version,
44
+ description: json.description,
45
+ publishedAt: (_a = json.time) === null || _a === void 0 ? void 0 : _a.modified,
46
+ });
47
+ }
48
+ catch {
49
+ reject(new Error('Failed to parse npm registry response'));
50
+ }
51
+ });
52
+ });
53
+ req.on('error', reject);
54
+ req.setTimeout(8000, () => { req.destroy(); reject(new Error('npm registry request timed out')); });
55
+ });
56
+ // @group Utilities : Compare semver strings — returns true if b is newer than a
57
+ const isNewer = (current, latest) => {
58
+ const parse = (v) => v.replace(/^v/, '').split('.').map(Number);
59
+ const [cMaj, cMin, cPat] = parse(current);
60
+ const [lMaj, lMin, lPat] = parse(latest);
61
+ if (lMaj !== cMaj)
62
+ return lMaj > cMaj;
63
+ if (lMin !== cMin)
64
+ return lMin > cMin;
65
+ return lPat > cPat;
66
+ };
67
+ const router = (0, express_1.Router)();
68
+ // @group CheckUpdate : GET /api/update/check — returns current vs latest npm version
69
+ router.get('/check', async (_req, res) => {
70
+ try {
71
+ const currentVersion = getCurrentVersion();
72
+ const { version: latestVersion, publishedAt } = await fetchNpmLatest();
73
+ const updateAvailable = isNewer(currentVersion, latestVersion);
74
+ const result = {
75
+ currentVersion,
76
+ latestVersion,
77
+ updateAvailable,
78
+ publishedAt,
79
+ };
80
+ res.json({ success: true, data: result });
81
+ }
82
+ catch (err) {
83
+ console.error('Update check failed:', err);
84
+ res.status(503).json({ success: false, error: err.message || 'Failed to reach npm registry' });
85
+ }
86
+ });
87
+ // @group InstallUpdate : POST /api/update/install — installs ezpm2gui@latest globally
88
+ // Streams progress lines as newline-delimited JSON (ndjson) so the client can read incrementally.
89
+ router.post('/install', (req, res) => {
90
+ res.setHeader('Content-Type', 'application/x-ndjson');
91
+ res.setHeader('Transfer-Encoding', 'chunked');
92
+ res.setHeader('Cache-Control', 'no-cache');
93
+ res.flushHeaders();
94
+ const send = (type, message) => {
95
+ res.write(JSON.stringify({ type, message }) + '\n');
96
+ };
97
+ send('log', 'Starting update — running npm install -g ezpm2gui@latest...');
98
+ // Use `npm` with execFile for safety — no shell injection possible
99
+ const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
100
+ const child = (0, child_process_1.spawn)(npmCmd, ['install', '-g', 'ezpm2gui@latest'], {
101
+ stdio: ['ignore', 'pipe', 'pipe'],
102
+ shell: false,
103
+ });
104
+ child.stdout.on('data', (chunk) => {
105
+ chunk.toString().split('\n').filter(Boolean).forEach((line) => send('log', line));
106
+ });
107
+ child.stderr.on('data', (chunk) => {
108
+ // npm writes progress to stderr — treat as log unless it's an actual error keyword
109
+ const text = chunk.toString();
110
+ const isError = /^npm (ERR|error)/i.test(text.trim());
111
+ text.split('\n').filter(Boolean).forEach((line) => send(isError ? 'error' : 'log', line));
112
+ });
113
+ child.on('close', (code) => {
114
+ if (code === 0) {
115
+ send('done', 'Update installed successfully. Reload the page to use the new frontend. Restart the server to apply backend changes.');
116
+ }
117
+ else {
118
+ send('fail', `npm exited with code ${code}. Update may have failed.`);
119
+ }
120
+ res.end();
121
+ });
122
+ child.on('error', (err) => {
123
+ send('fail', `Failed to run npm: ${err.message}`);
124
+ res.end();
125
+ });
126
+ });
127
+ // @group RestartServer : POST /api/update/restart — graceful server restart (relies on process manager to respawn)
128
+ router.post('/restart', (_req, res) => {
129
+ res.json({ success: true, message: 'Server will restart in 1 second.' });
130
+ setTimeout(() => {
131
+ console.log('[ezpm2gui] Graceful restart triggered via API.');
132
+ process.exit(0);
133
+ }, 1000);
134
+ });
135
+ exports.default = router;
@@ -54,6 +54,24 @@ export declare class RemoteConnection extends EventEmitter {
54
54
  * @param forceSudo Whether to force using sudo for this specific command
55
55
  */
56
56
  executeCommand(command: string, forceSudo?: boolean): Promise<CommandResult>;
57
+ /**
58
+ * Stream a remote file directly into an HTTP response without buffering.
59
+ *
60
+ * Strategy (tried in order):
61
+ * 1. SFTP createReadStream — best; handles large files, uses file-transfer protocol
62
+ * 2. exec `cat <file>` — current SSH user, direct pipe to response
63
+ * 3. exec `sudo -S cat` — password-based sudo (when password auth is configured)
64
+ * 4. exec `sudo cat` — NOPASSWD sudo (key-based auth where sudo needs no password)
65
+ */
66
+ private static readonly SHELL_UNSAFE;
67
+ private static validateRemotePath;
68
+ streamFileToResponse(remotePath: string, res: import('express').Response, fileName: string): Promise<void>;
69
+ /**
70
+ * Stream a remote .gz file to an HTTP response, decompressing it on the fly.
71
+ * Uses SFTP + local zlib.createGunzip() pipeline — no server-side buffering.
72
+ * Falls back to exec `zcat` if SFTP is unavailable.
73
+ */
74
+ streamGzFileToResponse(remotePath: string, res: import('express').Response, fileName: string): Promise<void>;
57
75
  /**
58
76
  * Check if PM2 is installed on the remote server
59
77
  * Uses multiple detection methods for better reliability
@@ -171,6 +171,170 @@ class RemoteConnection extends events_1.EventEmitter {
171
171
  });
172
172
  });
173
173
  }
174
+ static validateRemotePath(remotePath) {
175
+ if (!remotePath)
176
+ return false;
177
+ if (remotePath.includes('..'))
178
+ return false;
179
+ if (RemoteConnection.SHELL_UNSAFE.test(remotePath))
180
+ return false;
181
+ if (!/\.(log|gz)$/i.test(remotePath))
182
+ return false;
183
+ return true;
184
+ }
185
+ async streamFileToResponse(remotePath, res, fileName) {
186
+ // Validate path before any shell interpolation (fallback methods 2-4 use exec)
187
+ if (!RemoteConnection.validateRemotePath(remotePath)) {
188
+ if (!res.headersSent)
189
+ res.status(400).json({ success: false, error: 'Invalid log file path' });
190
+ return;
191
+ }
192
+ const setHeaders = () => {
193
+ if (!res.headersSent) {
194
+ res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
195
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
196
+ }
197
+ };
198
+ const escapeRemotePathForShell = (value) => {
199
+ if (/[\0\n\r]/.test(value)) {
200
+ throw new Error('Invalid remote path');
201
+ }
202
+ return `'${value.replace(/'/g, `'\"'\"'`)}'`;
203
+ };
204
+ let escapedRemotePath;
205
+ try {
206
+ escapedRemotePath = escapeRemotePathForShell(remotePath);
207
+ }
208
+ catch {
209
+ if (!res.headersSent) {
210
+ res.status(400).json({ success: false, error: 'Invalid remote path' });
211
+ }
212
+ return;
213
+ }
214
+ // @group StreamFileToResponse : Helper — pipe an exec channel stdout to response
215
+ const pipeExec = (cmd, sudoPassword) => new Promise((resolve) => {
216
+ this.client.exec(cmd, (err, channel) => {
217
+ if (err) {
218
+ resolve('error');
219
+ return;
220
+ }
221
+ if (sudoPassword) {
222
+ channel.stdin.write(sudoPassword + '\n');
223
+ }
224
+ channel.stdin.end();
225
+ let hasData = false;
226
+ channel.on('data', (chunk) => {
227
+ if (!hasData) {
228
+ setHeaders();
229
+ hasData = true;
230
+ }
231
+ res.write(chunk);
232
+ });
233
+ channel.on('close', (code) => {
234
+ if (hasData) {
235
+ res.end();
236
+ resolve('ok');
237
+ }
238
+ else
239
+ resolve(code === 0 ? 'empty' : 'error');
240
+ });
241
+ channel.on('error', () => resolve('error'));
242
+ channel.stderr.resume(); // Discard stderr so it doesn't block the channel
243
+ });
244
+ });
245
+ // ── 1. SFTP (preferred — real streaming, no exec overhead) ──────────
246
+ const sftpOk = await new Promise((resolve) => {
247
+ this.client.sftp((err, sftp) => {
248
+ if (err) {
249
+ resolve(false);
250
+ return;
251
+ }
252
+ const stream = sftp.createReadStream(remotePath);
253
+ let hasData = false;
254
+ stream.on('data', (chunk) => {
255
+ if (!hasData) {
256
+ setHeaders();
257
+ hasData = true;
258
+ }
259
+ res.write(chunk);
260
+ });
261
+ stream.on('end', () => { if (hasData) {
262
+ res.end();
263
+ resolve(true);
264
+ }
265
+ else
266
+ resolve(false); });
267
+ stream.on('error', () => resolve(false));
268
+ });
269
+ });
270
+ if (sftpOk)
271
+ return;
272
+ // ── 2. Plain cat ─────────────────────────────────────────────────────
273
+ if (await pipeExec(`cat -- ${escapedRemotePath}`) === 'ok')
274
+ return;
275
+ // ── 3. sudo -S cat (password auth) ───────────────────────────────────
276
+ if (this.config.password) {
277
+ if (await pipeExec(`sudo -S cat -- ${escapedRemotePath}`, this.config.password) === 'ok')
278
+ return;
279
+ }
280
+ // ── 4. sudo cat without -S (NOPASSWD / key-based auth) ───────────────
281
+ if (await pipeExec(`sudo cat -- ${escapedRemotePath}`) === 'ok')
282
+ return;
283
+ // All attempts failed
284
+ if (!res.headersSent) {
285
+ res.status(403).json({ success: false, error: 'Cannot read log file — check file permissions or sudo configuration' });
286
+ }
287
+ }
288
+ /**
289
+ * Stream a remote .gz file to an HTTP response, decompressing it on the fly.
290
+ * Uses SFTP + local zlib.createGunzip() pipeline — no server-side buffering.
291
+ * Falls back to exec `zcat` if SFTP is unavailable.
292
+ */
293
+ async streamGzFileToResponse(remotePath, res, fileName) {
294
+ if (!RemoteConnection.validateRemotePath(remotePath)) {
295
+ if (!res.headersSent)
296
+ res.status(400).json({ success: false, error: 'Invalid log file path' });
297
+ return;
298
+ }
299
+ const zlib = require('zlib');
300
+ const setHeaders = () => {
301
+ if (!res.headersSent) {
302
+ res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
303
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
304
+ }
305
+ };
306
+ // ── SFTP + local gunzip (preferred — no exec, streaming, no buffering) ──
307
+ const sftpOk = await new Promise((resolve) => {
308
+ this.client.sftp((err, sftp) => {
309
+ if (err) {
310
+ resolve(false);
311
+ return;
312
+ }
313
+ const inputStream = sftp.createReadStream(remotePath);
314
+ const gunzip = zlib.createGunzip();
315
+ setHeaders();
316
+ inputStream.pipe(gunzip).pipe(res);
317
+ gunzip.on('finish', () => resolve(true));
318
+ gunzip.on('error', () => { inputStream.destroy(); resolve(false); });
319
+ inputStream.on('error', () => resolve(false));
320
+ });
321
+ });
322
+ if (sftpOk)
323
+ return;
324
+ // ── Fallback: exec zcat and buffer (path already validated) ──────────
325
+ if (!res.headersSent) {
326
+ const result = await this.executeCommand(`zcat -- "${remotePath}" 2>/dev/null`);
327
+ const decompressed = result.stdout ||
328
+ (await this.executeCommand(`sudo zcat -- "${remotePath}" 2>/dev/null`)).stdout;
329
+ if (decompressed) {
330
+ setHeaders();
331
+ res.send(decompressed);
332
+ }
333
+ else if (!res.headersSent) {
334
+ res.status(500).json({ success: false, error: 'Could not decompress file' });
335
+ }
336
+ }
337
+ }
174
338
  /**
175
339
  * Check if PM2 is installed on the remote server
176
340
  * Uses multiple detection methods for better reliability
@@ -317,15 +481,47 @@ class RemoteConnection extends events_1.EventEmitter {
317
481
  }
318
482
  try {
319
483
  const processList = JSON.parse(cleanedOutput);
320
- return processList.map((proc) => ({
321
- name: proc.name,
322
- pm_id: proc.pm_id,
323
- status: proc.pm2_env ? proc.pm2_env.status : 'unknown',
324
- cpu: proc.monit ? (proc.monit.cpu || 0).toFixed(1) : '0.0',
325
- memory: proc.monit ? this.formatMemory(proc.monit.memory) : '0 B',
326
- uptime: proc.pm2_env ? this.formatUptime(proc.pm2_env.pm_uptime) : 'N/A',
327
- restarts: proc.pm2_env ? (proc.pm2_env.restart_time || 0) : 0
328
- }));
484
+ // Return the full PM2 process shape (pm2_env + monit) so all frontend
485
+ // components work without modification when viewing a remote server.
486
+ return processList.map((proc) => {
487
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v;
488
+ return ({
489
+ pid: proc.pid || 0,
490
+ pm_id: proc.pm_id || 0,
491
+ name: proc.name || '',
492
+ monit: {
493
+ cpu: proc.monit ? (proc.monit.cpu || 0) : 0,
494
+ memory: proc.monit ? (proc.monit.memory || 0) : 0,
495
+ },
496
+ pm2_env: {
497
+ // identity
498
+ pm_id: proc.pm_id || 0,
499
+ name: proc.name || '',
500
+ namespace: ((_a = proc.pm2_env) === null || _a === void 0 ? void 0 : _a.namespace) || 'default',
501
+ version: ((_b = proc.pm2_env) === null || _b === void 0 ? void 0 : _b.version) || '',
502
+ versioning: ((_c = proc.pm2_env) === null || _c === void 0 ? void 0 : _c.versioning) || null,
503
+ // timing
504
+ pm_uptime: ((_d = proc.pm2_env) === null || _d === void 0 ? void 0 : _d.pm_uptime) || 0,
505
+ created_at: ((_e = proc.pm2_env) === null || _e === void 0 ? void 0 : _e.created_at) || 0,
506
+ // paths
507
+ pm_cwd: ((_f = proc.pm2_env) === null || _f === void 0 ? void 0 : _f.pm_cwd) || '',
508
+ pm_exec_path: ((_g = proc.pm2_env) === null || _g === void 0 ? void 0 : _g.pm_exec_path) || '',
509
+ pm_out_log_path: ((_h = proc.pm2_env) === null || _h === void 0 ? void 0 : _h.pm_out_log_path) || '',
510
+ pm_err_log_path: ((_j = proc.pm2_env) === null || _j === void 0 ? void 0 : _j.pm_err_log_path) || '',
511
+ // runtime
512
+ exec_interpreter: ((_k = proc.pm2_env) === null || _k === void 0 ? void 0 : _k.exec_interpreter) || 'node',
513
+ exec_mode: ((_l = proc.pm2_env) === null || _l === void 0 ? void 0 : _l.exec_mode) || 'fork',
514
+ instances: ((_m = proc.pm2_env) === null || _m === void 0 ? void 0 : _m.instances) || 1,
515
+ node_args: ((_o = proc.pm2_env) === null || _o === void 0 ? void 0 : _o.node_args) || [],
516
+ status: ((_p = proc.pm2_env) === null || _p === void 0 ? void 0 : _p.status) || 'unknown',
517
+ restart_time: ((_q = proc.pm2_env) === null || _q === void 0 ? void 0 : _q.restart_time) || 0,
518
+ unstable_restarts: ((_r = proc.pm2_env) === null || _r === void 0 ? void 0 : _r.unstable_restarts) || 0,
519
+ autorestart: (_t = (_s = proc.pm2_env) === null || _s === void 0 ? void 0 : _s.autorestart) !== null && _t !== void 0 ? _t : true,
520
+ watch: ((_u = proc.pm2_env) === null || _u === void 0 ? void 0 : _u.watch) || false,
521
+ env: ((_v = proc.pm2_env) === null || _v === void 0 ? void 0 : _v.env) || {},
522
+ },
523
+ });
524
+ });
329
525
  }
330
526
  catch (error) {
331
527
  console.error('Error parsing PM2 process list:', error);
@@ -567,6 +763,17 @@ class RemoteConnection extends events_1.EventEmitter {
567
763
  }
568
764
  }
569
765
  exports.RemoteConnection = RemoteConnection;
766
+ /**
767
+ * Stream a remote file directly into an HTTP response without buffering.
768
+ *
769
+ * Strategy (tried in order):
770
+ * 1. SFTP createReadStream — best; handles large files, uses file-transfer protocol
771
+ * 2. exec `cat <file>` — current SSH user, direct pipe to response
772
+ * 3. exec `sudo -S cat` — password-based sudo (when password auth is configured)
773
+ * 4. exec `sudo cat` — NOPASSWD sudo (key-based auth where sudo needs no password)
774
+ */
775
+ // @group Security : Shell metacharacter allowlist for remote file paths
776
+ RemoteConnection.SHELL_UNSAFE = /['"`$|&;<>(){}\\\n\r\0]/;
570
777
  /**
571
778
  * Ordered list of command builders tried when resolving the pm2 binary.
572
779
  * The index of the first successful builder is cached so that subsequent
package/package.json CHANGED
@@ -1,71 +1,73 @@
1
- {
2
- "name": "ezpm2gui",
3
- "version": "1.4.0",
4
- "main": "dist/server/index.js",
5
- "bin": {
6
- "ezpm2gui": "bin/ezpm2gui.js",
7
- "ezpm2gui-generate-ecosystem": "bin/generate-ecosystem.js"
8
- },
9
- "scripts": {
10
- "start": "node dist/server/index.js",
11
- "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
12
- "dev:server": "nodemon --exec ts-node src/server/index.ts",
13
- "dev:client": "cd src/client && npm start",
14
- "build": "node scripts/build.js",
15
- "build:server": "tsc",
16
- "build:client": "cd src/client && npm run build",
17
- "build:bin": "tsc --project tsconfig.bin.json",
18
- "prepare": "npm run build",
19
- "test": "echo \"Error: no test specified\" && exit 1",
20
- "postinstall": "node scripts/postinstall.js"
21
- },
22
- "keywords": [
23
- "pm2",
24
- "gui",
25
- "monitor",
26
- "process-manager",
27
- "dashboard"
28
- ],
29
- "author": "Chandan Bhagat",
30
- "license": "ISC",
31
- "repository": {
32
- "type": "git",
33
- "url": "git+https://github.com/thechandanbhagat/ezpm2gui.git"
34
- },
35
- "description": "A modern web-based GUI for PM2 process manager",
36
- "files": [
37
- "dist/",
38
- "bin/",
39
- "src/client/build/",
40
- "scripts/postinstall.js"
41
- ],
42
- "dependencies": {
43
- "@tailwindcss/postcss": "^4.1.14",
44
- "@types/node-cron": "^3.0.11",
45
- "axios": "^1.9.0",
46
- "chart.js": "^4.4.9",
47
- "cron-parser": "^5.4.0",
48
- "express": "^4.18.3",
49
- "node-cron": "^4.2.1",
50
- "pm2": "^6.0.5",
51
- "react": "^19.1.0",
52
- "react-dom": "^19.1.0",
53
- "react-scripts": "^5.0.1",
54
- "socket.io": "^4.8.1",
55
- "ssh2": "^1.16.0",
56
- "uuid": "^11.0.5"
57
- },
58
- "devDependencies": {
59
- "@types/express": "^4.17.21",
60
- "@types/node": "^22.15.17",
61
- "@types/react": "^19.1.4",
62
- "@types/react-dom": "^19.1.5",
63
- "@types/socket.io": "^3.0.2",
64
- "@types/ssh2": "^1.15.5",
65
- "@types/uuid": "^10.0.0",
66
- "concurrently": "^9.1.2",
67
- "nodemon": "^3.1.10",
68
- "ts-node": "^10.9.2",
69
- "typescript": "^5.8.3"
70
- }
71
- }
1
+ {
2
+ "name": "ezpm2gui",
3
+ "version": "1.5.0",
4
+ "main": "dist/server/index.js",
5
+ "bin": {
6
+ "ezpm2gui": "bin/ezpm2gui.js",
7
+ "ezpm2gui-generate-ecosystem": "bin/generate-ecosystem.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node dist/server/index.js",
11
+ "dev": "concurrently --kill-others-on-fail --names \"server,client\" --prefix-colors \"cyan,green\" \"npm run dev:server\" \"npm run dev:client\"",
12
+ "dev:server": "nodemon --exec ts-node src/server/index.ts",
13
+ "dev:client": "cd src/client && npm run dev",
14
+ "build": "node scripts/build.js",
15
+ "build:server": "tsc",
16
+ "build:client": "cd src/client && npm run build",
17
+ "build:bin": "tsc --project tsconfig.bin.json",
18
+ "prepare": "npm run build",
19
+ "test": "echo \"Error: no test specified\" && exit 1",
20
+ "postinstall": "node scripts/postinstall.js"
21
+ },
22
+ "keywords": [
23
+ "pm2",
24
+ "gui",
25
+ "monitor",
26
+ "process-manager",
27
+ "dashboard"
28
+ ],
29
+ "author": "Chandan Bhagat",
30
+ "license": "ISC",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/thechandanbhagat/ezpm2gui.git"
34
+ },
35
+ "description": "A modern web-based GUI for PM2 process manager",
36
+ "files": [
37
+ "dist/",
38
+ "bin/",
39
+ "src/client/build/",
40
+ "scripts/postinstall.js"
41
+ ],
42
+ "dependencies": {
43
+ "@tailwindcss/postcss": "^4.1.14",
44
+ "@types/node-cron": "^3.0.11",
45
+ "axios": "^1.9.0",
46
+ "dotenv": "^16.5.0",
47
+ "chart.js": "^4.4.9",
48
+ "cron-parser": "^5.4.0",
49
+ "express": "^4.18.3",
50
+ "node-cron": "^4.2.1",
51
+ "pm2": "^6.0.5",
52
+ "react": "^19.1.0",
53
+ "react-dom": "^19.1.0",
54
+ "react-scripts": "^5.0.1",
55
+ "socket.io": "^4.8.1",
56
+ "ssh2": "^1.16.0",
57
+ "uuid": "^11.0.5"
58
+ },
59
+ "devDependencies": {
60
+ "@types/express": "^4.17.21",
61
+ "@types/node": "^22.15.17",
62
+ "@types/react": "^19.1.4",
63
+ "@types/react-dom": "^19.1.5",
64
+ "@types/socket.io": "^3.0.2",
65
+ "@types/ssh2": "^1.15.5",
66
+ "@types/uuid": "^10.0.0",
67
+ "concurrently": "^9.1.2",
68
+ "cross-env": "^10.1.0",
69
+ "nodemon": "^3.1.10",
70
+ "ts-node": "^10.9.2",
71
+ "typescript": "^5.8.3"
72
+ }
73
+ }
@@ -1,36 +1,36 @@
1
- const { execSync } = require('child_process');
2
- const fs = require('fs');
3
- const path = require('path');
4
-
5
- console.log('Running postinstall script...');
6
-
7
- const isGlobalInstall = process.env.npm_config_global === 'true';
8
- if (isGlobalInstall) {
9
- console.log('Global installation detected, skipping client dependencies.');
10
- process.exit(0);
11
- }
12
-
13
- // Check if we're in a development environment
14
- const clientDir = path.join(__dirname, '..', 'src', 'client');
15
- const clientNodeModules = path.join(clientDir, 'node_modules');
16
- const packageJsonPath = path.join(clientDir, 'package.json');
17
-
18
- if (fs.existsSync(packageJsonPath) && !fs.existsSync(clientNodeModules)) {
19
- console.log('Installing client dependencies...');
20
- try {
21
- // Change to client directory
22
- process.chdir(clientDir);
23
-
24
- // Install dependencies
25
- execSync('npm install', { stdio: 'inherit' });
26
-
27
- console.log('Client dependencies installed successfully.');
28
- } catch (error) {
29
- console.error('Error installing client dependencies:', error.message);
30
- console.error('Please run "cd src/client && npm install" manually.');
31
- }
32
- } else {
33
- console.log('Client dependencies already installed or client package.json not found.');
34
- }
35
-
36
- console.log('Postinstall completed.');
1
+ const { execSync } = require('child_process');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ console.log('Running postinstall script...');
6
+
7
+ const isGlobalInstall = process.env.npm_config_global === 'true';
8
+ if (isGlobalInstall) {
9
+ console.log('Global installation detected, skipping client dependencies.');
10
+ process.exit(0);
11
+ }
12
+
13
+ // Check if we're in a development environment
14
+ const clientDir = path.join(__dirname, '..', 'src', 'client');
15
+ const clientNodeModules = path.join(clientDir, 'node_modules');
16
+ const packageJsonPath = path.join(clientDir, 'package.json');
17
+
18
+ if (fs.existsSync(packageJsonPath) && !fs.existsSync(clientNodeModules)) {
19
+ console.log('Installing client dependencies...');
20
+ try {
21
+ // Change to client directory
22
+ process.chdir(clientDir);
23
+
24
+ // Install dependencies
25
+ execSync('npm install', { stdio: 'inherit' });
26
+
27
+ console.log('Client dependencies installed successfully.');
28
+ } catch (error) {
29
+ console.error('Error installing client dependencies:', error.message);
30
+ console.error('Please run "cd src/client && npm install" manually.');
31
+ }
32
+ } else {
33
+ console.log('Client dependencies already installed or client package.json not found.');
34
+ }
35
+
36
+ console.log('Postinstall completed.');
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "files": {
3
- "main.css": "/static/css/main.c506cba5.css",
4
- "main.js": "/static/js/main.5278cddd.js",
3
+ "main.css": "/static/css/main.2d095544.css",
4
+ "main.js": "/static/js/main.17e17668.js",
5
5
  "index.html": "/index.html",
6
- "main.c506cba5.css.map": "/static/css/main.c506cba5.css.map",
7
- "main.5278cddd.js.map": "/static/js/main.5278cddd.js.map"
6
+ "main.2d095544.css.map": "/static/css/main.2d095544.css.map",
7
+ "main.17e17668.js.map": "/static/js/main.17e17668.js.map"
8
8
  },
9
9
  "entrypoints": [
10
- "static/css/main.c506cba5.css",
11
- "static/js/main.5278cddd.js"
10
+ "static/css/main.2d095544.css",
11
+ "static/js/main.17e17668.js"
12
12
  ]
13
13
  }
@@ -1,2 +1,2 @@
1
- <!-- Simple favicon content - binary content approximated with HTML comment -->
2
- <!-- This is a placeholder and should be replaced with a proper ICO file -->
1
+ <!-- Simple favicon content - binary content approximated with HTML comment -->
2
+ <!-- This is a placeholder and should be replaced with a proper ICO file -->