ezpm2gui 1.3.2 → 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 (44) hide show
  1. package/README.md +295 -294
  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 -236
  8. package/dist/server/index.js +256 -83
  9. package/dist/server/routes/deployApplication.js +6 -5
  10. package/dist/server/routes/logStreaming.js +20 -13
  11. package/dist/server/routes/modules.js +89 -69
  12. package/dist/server/routes/remoteConnections.js +279 -40
  13. package/dist/server/routes/updates.d.ts +3 -0
  14. package/dist/server/routes/updates.js +135 -0
  15. package/dist/server/utils/encryption.js +0 -12
  16. package/dist/server/utils/pm2-connection.d.ts +1 -1
  17. package/dist/server/utils/pm2-connection.js +1 -3
  18. package/dist/server/utils/remote-connection.d.ts +36 -3
  19. package/dist/server/utils/remote-connection.js +307 -79
  20. package/package.json +73 -69
  21. package/scripts/postinstall.js +36 -36
  22. package/src/client/build/asset-manifest.json +6 -6
  23. package/src/client/build/favicon.ico +2 -2
  24. package/src/client/build/index.html +1 -1
  25. package/src/client/build/logo192.svg +7 -7
  26. package/src/client/build/logo512.svg +7 -7
  27. package/src/client/build/manifest.json +24 -24
  28. package/src/client/build/static/css/main.2d095544.css +5 -0
  29. package/src/client/build/static/css/main.2d095544.css.map +1 -0
  30. package/src/client/build/static/js/main.17e17668.js +3 -0
  31. package/src/client/build/static/js/main.17e17668.js.map +1 -0
  32. package/dist/server/config/cron-jobs.json +0 -18
  33. package/dist/server/config/cron-scripts/6d8d5e1d-2bc8-463f-82a6-6c294f2b9dbe.sh +0 -2
  34. package/dist/server/config/remote-connections.json +0 -22
  35. package/dist/server/logs/deployment.log +0 -12
  36. package/dist/server/utils/dialog.d.ts +0 -1
  37. package/dist/server/utils/dialog.js +0 -16
  38. package/dist/server/utils/upload.d.ts +0 -3
  39. package/dist/server/utils/upload.js +0 -39
  40. package/src/client/build/static/css/main.d46bc75c.css +0 -5
  41. package/src/client/build/static/css/main.d46bc75c.css.map +0 -1
  42. package/src/client/build/static/js/main.b0e1c9b1.js +0 -3
  43. package/src/client/build/static/js/main.b0e1c9b1.js.map +0 -1
  44. /package/src/client/build/static/js/{main.b0e1c9b1.js.LICENSE.txt → main.17e17668.js.LICENSE.txt} +0 -0
@@ -34,6 +34,8 @@ class RemoteConnection extends events_1.EventEmitter {
34
34
  super();
35
35
  this._isConnected = false;
36
36
  this.isPM2Installed = false;
37
+ /** Index into PM2_BUILDERS of the invocation that actually works on this host. -1 = not yet resolved. */
38
+ this.resolvedBuilderIndex = -1;
37
39
  this.client = new ssh2_1.Client();
38
40
  this.config = config;
39
41
  // Initialize public properties
@@ -127,11 +129,10 @@ class RemoteConnection extends events_1.EventEmitter {
127
129
  }
128
130
  // Apply sudo if configured or explicitly requested for this command
129
131
  const useElevatedPrivileges = forceSudo || this.config.useSudo;
130
- let finalCommand = command;
131
- // Only apply sudo if it's requested and we have a password
132
- if (useElevatedPrivileges && this.config.password) {
133
- finalCommand = `echo '${this.config.password}' | sudo -S ${command}`;
134
- }
132
+ // Use sudo -S so sudo reads the password from stdin instead of piping it in the shell command
133
+ const finalCommand = (useElevatedPrivileges && this.config.password)
134
+ ? `sudo -S ${command}`
135
+ : command;
135
136
  console.log(`Executing command: ${useElevatedPrivileges ? '[sudo] ' : ''}${command}`);
136
137
  return new Promise((resolve, reject) => {
137
138
  this.client.exec(finalCommand, (err, channel) => {
@@ -139,6 +140,11 @@ class RemoteConnection extends events_1.EventEmitter {
139
140
  console.error('Error executing command:', err);
140
141
  return reject(err);
141
142
  }
143
+ // Write password to sudo's stdin, then close stdin
144
+ if (useElevatedPrivileges && this.config.password) {
145
+ channel.stdin.write(this.config.password + '\n');
146
+ channel.stdin.end();
147
+ }
142
148
  let stdout = '';
143
149
  let stderr = '';
144
150
  let exitCode = null;
@@ -152,11 +158,6 @@ class RemoteConnection extends events_1.EventEmitter {
152
158
  exitCode = code;
153
159
  });
154
160
  channel.on('close', () => {
155
- // Remove sudo password from stdout/stderr if present
156
- if (useElevatedPrivileges && this.config.password) {
157
- stdout = stdout.replace(this.config.password, '[PASSWORD REDACTED]');
158
- stderr = stderr.replace(this.config.password, '[PASSWORD REDACTED]');
159
- }
160
161
  resolve({
161
162
  stdout,
162
163
  stderr,
@@ -170,6 +171,170 @@ class RemoteConnection extends events_1.EventEmitter {
170
171
  });
171
172
  });
172
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
+ }
173
338
  /**
174
339
  * Check if PM2 is installed on the remote server
175
340
  * Uses multiple detection methods for better reliability
@@ -246,91 +411,122 @@ class RemoteConnection extends events_1.EventEmitter {
246
411
  }
247
412
  }
248
413
  /**
249
- * Execute a PM2 command with proper PATH handling
250
- * Tries different methods to find and execute PM2
414
+ * Execute a PM2 command with proper PATH handling.
415
+ * The first successful invocation is cached so later calls (and streaming
416
+ * commands) reuse the same shell/binary path without re-probing.
251
417
  */
252
418
  async executePM2Command(pm2Args) {
253
- const commands = [
254
- `pm2 ${pm2Args}`, // Direct PM2 call
255
- `bash -l -c "pm2 ${pm2Args}"`, // With login shell
256
- `~/.npm-global/bin/pm2 ${pm2Args}`, // Common global npm path
257
- `~/node_modules/.bin/pm2 ${pm2Args}`, // Local node_modules path
258
- `/usr/local/bin/pm2 ${pm2Args}`, // System-wide installation
259
- `npx pm2 ${pm2Args}` // Using npx as fallback
260
- ];
419
+ // Fast path: reuse the builder we already know works on this host
420
+ if (this.resolvedBuilderIndex >= 0) {
421
+ return this.executeCommand(RemoteConnection.PM2_BUILDERS[this.resolvedBuilderIndex](pm2Args));
422
+ }
261
423
  let lastError = null;
262
- for (const command of commands) {
424
+ for (let i = 0; i < RemoteConnection.PM2_BUILDERS.length; i++) {
425
+ const command = RemoteConnection.PM2_BUILDERS[i](pm2Args);
263
426
  try {
264
427
  const result = await this.executeCommand(command);
265
428
  if (result.code === 0) {
429
+ this.resolvedBuilderIndex = i; // Cache for all future calls on this connection
430
+ console.log(`[executePM2Command] Resolved pm2 via builder[${i}]: ${command.substring(0, 60)}`);
266
431
  return result;
267
432
  }
268
- lastError = new Error(`Command failed with exit code ${result.code}: ${result.stderr}`);
433
+ lastError = new Error(`Command failed (code ${result.code}): ${result.stderr}`);
269
434
  }
270
435
  catch (error) {
271
436
  lastError = error;
272
- continue;
273
437
  }
274
438
  }
275
439
  throw lastError || new Error(`Failed to execute PM2 command: ${pm2Args}`);
276
440
  }
441
+ /**
442
+ * Build a pm2 command string using the already-resolved invocation pattern.
443
+ * Falls back to bash -li -c if the resolver has not yet run.
444
+ * Use this when you need the raw command string for a streaming shell exec.
445
+ */
446
+ buildPM2StreamCommand(pm2Args) {
447
+ if (this.resolvedBuilderIndex >= 0) {
448
+ return RemoteConnection.PM2_BUILDERS[this.resolvedBuilderIndex](pm2Args);
449
+ }
450
+ // Default: login+interactive shell covers .profile AND .bashrc (catches nvm)
451
+ return `bash -li -c "pm2 ${pm2Args}"`;
452
+ }
277
453
  /**
278
454
  * Get PM2 processes from the remote server
279
455
  */
280
456
  async getPM2Processes() {
457
+ if (!this._isConnected) {
458
+ await this.connect();
459
+ }
460
+ let result;
281
461
  try {
282
- // Connect if not already connected
283
- if (!this._isConnected) {
284
- await this.connect();
285
- }
286
- // Check if PM2 is installed (force re-check if not cached)
287
- const isPM2Installed = await this.checkPM2Installation();
288
- if (!isPM2Installed) {
289
- throw new Error('PM2 is not installed on the remote server. Please install PM2 globally using: npm install -g pm2');
290
- }
291
- // Update the cached status
462
+ // executePM2Command tries all known paths/shells before giving up
463
+ result = await this.executePM2Command('jlist');
292
464
  this.isPM2Installed = true;
293
- const result = await this.executePM2Command('jlist');
294
- // Clean the output to ensure it's valid JSON
295
- // Sometimes pm2 jlist can include non-JSON data at the beginning or end
296
- let cleanedOutput = result.stdout.trim();
297
- // Find the beginning of the JSON array
298
- const startIndex = cleanedOutput.indexOf('[');
299
- // Find the end of the JSON array
300
- const endIndex = cleanedOutput.lastIndexOf(']') + 1;
301
- if (startIndex === -1 || endIndex === 0) {
302
- console.log('Invalid PM2 output format, trying alternative approach');
303
- // Try using pm2 list --format=json instead
304
- const listResult = await this.executeCommand('pm2 list --format=json');
305
- cleanedOutput = listResult.stdout.trim();
306
- }
307
- else if (startIndex > 0 || endIndex < cleanedOutput.length) {
308
- // Extract just the JSON array part
309
- cleanedOutput = cleanedOutput.substring(startIndex, endIndex);
310
- }
311
- try {
312
- const processList = JSON.parse(cleanedOutput);
313
- // Format process data similar to local PM2 format
314
- return processList.map((proc) => ({
315
- name: proc.name,
316
- pm_id: proc.pm_id,
317
- status: proc.pm2_env ? proc.pm2_env.status : 'unknown',
318
- cpu: proc.monit ? (proc.monit.cpu || 0).toFixed(1) : '0.0',
319
- memory: proc.monit ? this.formatMemory(proc.monit.memory) : '0 B',
320
- uptime: proc.pm2_env ? this.formatUptime(proc.pm2_env.pm_uptime) : 'N/A',
321
- restarts: proc.pm2_env ? (proc.pm2_env.restart_time || 0) : 0
322
- }));
323
- }
324
- catch (error) {
325
- console.error('Error parsing PM2 process list:', error);
326
- console.log('Raw output:', result.stdout);
327
- // Return an empty array instead of throwing
328
- return [];
329
- }
465
+ }
466
+ catch (err) {
467
+ // All path attempts failed PM2 is genuinely not found
468
+ console.error('Error getting PM2 processes:', err);
469
+ throw new Error('PM2 is not installed or not accessible on the remote server. Please install PM2 globally using: npm install -g pm2');
470
+ }
471
+ // Extract the JSON array from the output (pm2 jlist may include extra lines)
472
+ let cleanedOutput = result.stdout.trim();
473
+ const startIndex = cleanedOutput.indexOf('[');
474
+ const endIndex = cleanedOutput.lastIndexOf(']') + 1;
475
+ if (startIndex !== -1 && endIndex > 0) {
476
+ cleanedOutput = cleanedOutput.substring(startIndex, endIndex);
477
+ }
478
+ else {
479
+ console.log('Invalid PM2 jlist output, returning empty list. Raw:', result.stdout);
480
+ return [];
481
+ }
482
+ try {
483
+ const processList = JSON.parse(cleanedOutput);
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
+ });
330
525
  }
331
526
  catch (error) {
332
- console.error('Error getting PM2 processes:', error);
333
- throw error;
527
+ console.error('Error parsing PM2 process list:', error);
528
+ console.log('Raw output:', result.stdout);
529
+ return [];
334
530
  }
335
531
  }
336
532
  /**
@@ -506,14 +702,11 @@ class RemoteConnection extends events_1.EventEmitter {
506
702
  if (!this._isConnected) {
507
703
  throw new Error('Connection not established');
508
704
  }
509
- let finalCommand = command;
705
+ // Use sudo -S so sudo reads the password from stdin rather than the shell command line
706
+ const useSudoAuth = useSudo && this.config.useSudo && !!this.config.password;
707
+ const finalCommand = useSudoAuth ? `sudo -S ${command}` : command;
510
708
  let isInitialized = false;
511
- // Apply sudo if requested and we have a password
512
- if (useSudo && this.config.useSudo && this.config.password) {
513
- // Use echo to pipe the password to sudo for streaming commands
514
- finalCommand = `echo '${this.config.password}' | sudo -S ${command}`;
515
- }
516
- console.log(`[createLogStream] Executing command: ${finalCommand.replace(this.config.password || '', '[PASSWORD]')}`);
709
+ console.log(`[createLogStream] Executing command: ${useSudoAuth ? '[sudo] ' : ''}${command}`);
517
710
  return new Promise((resolve, reject) => {
518
711
  this.client.exec(finalCommand, (err, stream) => {
519
712
  var _a;
@@ -522,6 +715,10 @@ class RemoteConnection extends events_1.EventEmitter {
522
715
  reject(err);
523
716
  return;
524
717
  }
718
+ // Write password to sudo's stdin (do not close stdin — stream must stay open)
719
+ if (useSudoAuth && this.config.password) {
720
+ stream.stdin.write(this.config.password + '\n');
721
+ }
525
722
  const logEmitter = new events_1.EventEmitter();
526
723
  stream.on('data', (data) => {
527
724
  const dataStr = data.toString();
@@ -566,6 +763,37 @@ class RemoteConnection extends events_1.EventEmitter {
566
763
  }
567
764
  }
568
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]/;
777
+ /**
778
+ * Ordered list of command builders tried when resolving the pm2 binary.
779
+ * The index of the first successful builder is cached so that subsequent
780
+ * calls (including streaming commands) reuse the same invocation.
781
+ */
782
+ RemoteConnection.PM2_BUILDERS = [
783
+ args => `pm2 ${args}`,
784
+ args => `bash -l -c "pm2 ${args}"`,
785
+ args => `bash -i -c "pm2 ${args}"`,
786
+ args => `bash -li -c "pm2 ${args}"`,
787
+ args => `/usr/local/bin/pm2 ${args}`,
788
+ args => `/usr/bin/pm2 ${args}`,
789
+ args => `~/.npm-global/bin/pm2 ${args}`,
790
+ args => `~/.npm/bin/pm2 ${args}`,
791
+ args => `~/node_modules/.bin/pm2 ${args}`,
792
+ args => `. ~/.nvm/nvm.sh && pm2 ${args}`,
793
+ args => `. ~/.nvm/nvm.sh && nvm use --lts && pm2 ${args}`,
794
+ args => `. /usr/share/nvm/init-nvm.sh && pm2 ${args}`,
795
+ args => `npx pm2 ${args}`,
796
+ ];
569
797
  /**
570
798
  * Connection manager to handle multiple remote connections
571
799
  */
package/package.json CHANGED
@@ -1,69 +1,73 @@
1
- {
2
- "name": "ezpm2gui",
3
- "version": "1.3.2",
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
- },
57
- "devDependencies": {
58
- "@types/express": "^4.17.21",
59
- "@types/node": "^22.15.17",
60
- "@types/react": "^19.1.4",
61
- "@types/react-dom": "^19.1.5",
62
- "@types/socket.io": "^3.0.2",
63
- "@types/ssh2": "^1.15.5",
64
- "concurrently": "^9.1.2",
65
- "nodemon": "^3.1.10",
66
- "ts-node": "^10.9.2",
67
- "typescript": "^5.8.3"
68
- }
69
- }
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
+ }