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
@@ -4,10 +4,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createServer = createServer;
7
+ const path_1 = __importDefault(require("path"));
8
+ // @group Configuration : Load .env.local first (local overrides), then .env (defaults)
9
+ // Must happen before any code reads process.env
10
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
11
+ const dotenv = require('dotenv');
12
+ dotenv.config({ path: path_1.default.resolve(process.cwd(), '.env.local') });
13
+ dotenv.config({ path: path_1.default.resolve(process.cwd(), '.env') });
7
14
  const express_1 = __importDefault(require("express"));
8
15
  const http_1 = __importDefault(require("http"));
9
16
  const socket_io_1 = require("socket.io");
10
- const path_1 = __importDefault(require("path"));
11
17
  const pm2_1 = __importDefault(require("pm2"));
12
18
  const os_1 = __importDefault(require("os"));
13
19
  const processConfig_1 = __importDefault(require("./routes/processConfig"));
@@ -15,6 +21,7 @@ const deployApplication_1 = __importDefault(require("./routes/deployApplication"
15
21
  const modules_1 = __importDefault(require("./routes/modules"));
16
22
  const remoteConnections_1 = __importDefault(require("./routes/remoteConnections"));
17
23
  const cronJobs_1 = __importDefault(require("./routes/cronJobs"));
24
+ const updates_1 = __importDefault(require("./routes/updates"));
18
25
  const logStreaming_1 = require("./routes/logStreaming");
19
26
  const pm2_connection_1 = require("./utils/pm2-connection");
20
27
  const remote_connection_1 = require("./utils/remote-connection");
@@ -29,7 +36,16 @@ function createServer() {
29
36
  cors: {
30
37
  origin: '*',
31
38
  methods: ['GET', 'POST']
32
- }
39
+ },
40
+ // Increase ping timeout to prevent false positive disconnections
41
+ pingTimeout: 10000, // How long to wait for a pong response (default: 5000ms)
42
+ pingInterval: 25000, // How often to send ping packets (default: 25000ms)
43
+ // Allow reconnection attempts
44
+ allowEIO3: true,
45
+ // Transport configuration
46
+ transports: ['websocket', 'polling'],
47
+ // Upgrade timeout
48
+ upgradeTimeout: 10000
33
49
  });
34
50
  // Configure middleware
35
51
  app.use(express_1.default.json());
@@ -49,6 +65,7 @@ function createServer() {
49
65
  app.use('/api/modules', modules_1.default);
50
66
  app.use('/api/remote', remoteConnections_1.default);
51
67
  app.use('/api/cron-jobs', cronJobs_1.default);
68
+ app.use('/api/update', updates_1.default);
52
69
  // Setup log streaming with Socket.IO
53
70
  (0, logStreaming_1.setupLogStreaming)(io); // PM2 API endpoints
54
71
  app.get('/api/processes', async (req, res) => {
@@ -73,42 +90,36 @@ function createServer() {
73
90
  }
74
91
  });
75
92
  // Action endpoints (start, stop, restart, delete)
76
- app.post('/api/process/:id/:action', (req, res) => {
93
+ app.post('/api/process/:id/:action', async (req, res) => {
77
94
  const { id, action } = req.params;
78
- pm2_1.default.connect((err) => {
79
- if (err) {
80
- console.error(err);
81
- res.status(500).json({ error: 'Failed to connect to PM2' });
82
- return;
83
- }
84
- const processAction = (actionName, cb) => {
85
- switch (actionName) {
95
+ const validActions = ['start', 'stop', 'restart', 'delete'];
96
+ if (!validActions.includes(action)) {
97
+ res.status(400).json({ error: 'Invalid action' });
98
+ return;
99
+ }
100
+ try {
101
+ await (0, pm2_connection_1.executePM2Command)((callback) => {
102
+ switch (action) {
86
103
  case 'start':
87
- pm2_1.default.start(id, cb);
104
+ pm2_1.default.start(id, callback);
88
105
  break;
89
106
  case 'stop':
90
- pm2_1.default.stop(id, cb);
107
+ pm2_1.default.stop(id, callback);
91
108
  break;
92
109
  case 'restart':
93
- pm2_1.default.restart(id, cb);
110
+ pm2_1.default.restart(id, callback);
94
111
  break;
95
112
  case 'delete':
96
- pm2_1.default.delete(id, cb);
113
+ pm2_1.default.delete(id, callback);
97
114
  break;
98
- default:
99
- cb(new Error('Invalid action'));
100
115
  }
101
- };
102
- processAction(action, (err) => {
103
- pm2_1.default.disconnect();
104
- if (err) {
105
- console.error(err);
106
- res.status(500).json({ error: `Failed to ${action} process` });
107
- return;
108
- }
109
- res.json({ success: true, message: `Process ${id} ${action} request received` });
110
116
  });
111
- });
117
+ res.json({ success: true, message: `Process ${id} ${action} request received` });
118
+ }
119
+ catch (err) {
120
+ console.error(err);
121
+ res.status(500).json({ error: `Failed to ${action} process` });
122
+ }
112
123
  });
113
124
  // Get system metrics
114
125
  app.get('/api/metrics', (req, res) => {
@@ -124,76 +135,238 @@ function createServer() {
124
135
  };
125
136
  res.json(metrics);
126
137
  });
127
- // Get process logs
128
- app.get('/api/logs/:id/:type', (req, res) => {
138
+ // @group LogHistory : Resolve log path from PM2 process descriptor
139
+ const resolveLocalLogPath = async (id, logType) => {
140
+ var _a, _b, _c;
141
+ const processDesc = await (0, pm2_connection_1.executePM2Command)((callback) => {
142
+ pm2_1.default.describe(id, callback);
143
+ });
144
+ if (!processDesc || processDesc.length === 0)
145
+ return null;
146
+ return (_c = (_b = (_a = processDesc[0]) === null || _a === void 0 ? void 0 : _a.pm2_env) === null || _b === void 0 ? void 0 : _b[`pm_${logType}_log_path`]) !== null && _c !== void 0 ? _c : null;
147
+ };
148
+ // @group LogHistory : Get log lines — ?lines=N (default 200, 0 = all)
149
+ app.get('/api/logs/:id/:type', async (req, res) => {
129
150
  const { id, type } = req.params;
130
151
  const logType = type === 'err' ? 'err' : 'out';
131
- pm2_1.default.connect((err) => {
132
- if (err) {
133
- console.error(err);
134
- res.status(500).json({ error: 'Failed to connect to PM2' });
152
+ const lines = parseInt(req.query.lines || '200', 10);
153
+ try {
154
+ const logPath = await resolveLocalLogPath(id, logType);
155
+ if (!logPath) {
156
+ res.status(404).json({ error: 'Process not found or log path unavailable' });
135
157
  return;
136
158
  }
137
- pm2_1.default.describe(id, (err, processDesc) => {
138
- var _a, _b;
139
- if (err || !processDesc || processDesc.length === 0) {
140
- pm2_1.default.disconnect();
141
- res.status(404).json({ error: 'Process not found' });
142
- return;
159
+ const fs = require('fs');
160
+ if (!fs.existsSync(logPath)) {
161
+ res.json({ logs: [], logPath });
162
+ return;
163
+ }
164
+ const { lines: result, total } = await streamTailLines(fs.createReadStream(logPath), lines);
165
+ res.json({ logs: result, logPath, totalLines: total });
166
+ }
167
+ catch (err) {
168
+ console.error(`Error reading log file: ${err}`);
169
+ res.status(500).json({ error: 'Failed to read log file' });
170
+ }
171
+ });
172
+ // @group LogHistory : Download full log file
173
+ app.get('/api/logs/:id/:type/download', async (req, res) => {
174
+ const { id, type } = req.params;
175
+ const logType = type === 'err' ? 'err' : 'out';
176
+ try {
177
+ const logPath = await resolveLocalLogPath(id, logType);
178
+ if (!logPath) {
179
+ res.status(404).json({ error: 'Process not found or log path unavailable' });
180
+ return;
181
+ }
182
+ const fs = require('fs');
183
+ if (!fs.existsSync(logPath)) {
184
+ res.status(404).json({ error: 'Log file does not exist yet' });
185
+ return;
186
+ }
187
+ const fileName = `${id}-${logType}.log`;
188
+ res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
189
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
190
+ fs.createReadStream(logPath).pipe(res);
191
+ }
192
+ catch (err) {
193
+ console.error(`Error downloading log file: ${err}`);
194
+ res.status(500).json({ error: 'Failed to download log file' });
195
+ }
196
+ });
197
+ // @group LogHistory : List all log files (current + rotated) for a process
198
+ // Uses /api/log-files/:id to avoid Express matching /:id/:type with type='files'
199
+ app.get('/api/log-files/:id', async (req, res) => {
200
+ const { id } = req.params;
201
+ try {
202
+ const fs = require('fs');
203
+ const nodePath = require('path');
204
+ const [outPath, errPath] = await Promise.all([
205
+ resolveLocalLogPath(id, 'out'),
206
+ resolveLocalLogPath(id, 'err'),
207
+ ]);
208
+ if (!outPath && !errPath) {
209
+ res.status(404).json({ error: 'Process not found or no log paths available' });
210
+ return;
211
+ }
212
+ // Derive the process base name from the log path (strip -out.log suffix)
213
+ const baseName = outPath
214
+ ? nodePath.basename(outPath).replace(/-out\.log.*$/, '')
215
+ : nodePath.basename(errPath).replace(/-(error|err)\.log.*$/, '');
216
+ const logDirs = new Set();
217
+ if (outPath)
218
+ logDirs.add(nodePath.dirname(outPath));
219
+ if (errPath)
220
+ logDirs.add(nodePath.dirname(errPath));
221
+ const files = [];
222
+ for (const dir of logDirs) {
223
+ if (!fs.existsSync(dir))
224
+ continue;
225
+ for (const fileName of fs.readdirSync(dir)) {
226
+ if (!fileName.startsWith(baseName))
227
+ continue;
228
+ const filePath = nodePath.join(dir, fileName);
229
+ const stat = fs.statSync(filePath);
230
+ if (!stat.isFile())
231
+ continue;
232
+ let type = 'unknown';
233
+ if (fileName.includes('-out'))
234
+ type = 'out';
235
+ else if (fileName.includes('-error') || fileName.includes('-err'))
236
+ type = 'err';
237
+ files.push({
238
+ name: fileName,
239
+ path: filePath,
240
+ size: stat.size,
241
+ modified: stat.mtime.toISOString(),
242
+ type,
243
+ compressed: fileName.endsWith('.gz'),
244
+ });
143
245
  }
144
- const logPath = (_b = (_a = processDesc[0]) === null || _a === void 0 ? void 0 : _a.pm2_env) === null || _b === void 0 ? void 0 : _b[`pm_${logType}_log_path`];
145
- if (!logPath) {
146
- pm2_1.default.disconnect();
147
- res.status(404).json({ error: `Log file for ${logType} not found` });
246
+ }
247
+ files.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
248
+ res.json({ files });
249
+ }
250
+ catch (err) {
251
+ console.error('Error listing log files:', err);
252
+ res.status(500).json({ error: 'Failed to list log files' });
253
+ }
254
+ });
255
+ // @group LogHistory : Security helper — block path traversal; allow any absolute log file path
256
+ const isAllowedLogPath = (filePath) => {
257
+ const nodePath = require('path');
258
+ const norm = nodePath.normalize(filePath);
259
+ // Require absolute path; reject shell-dangerous characters; require log extension
260
+ const SHELL_UNSAFE = /['"`;$|&<>(){}\\\n\r\0]/;
261
+ return (nodePath.isAbsolute(norm) &&
262
+ !norm.includes('..') &&
263
+ !SHELL_UNSAFE.test(norm) &&
264
+ /\.(log|gz)$/i.test(norm));
265
+ };
266
+ // @group LogHistory : Stream last N lines from a readable stream (ring buffer, no full-file load)
267
+ const streamTailLines = (inputStream, maxLines) => {
268
+ return new Promise((resolve, reject) => {
269
+ const rl = require('readline').createInterface({ input: inputStream, crlfDelay: Infinity });
270
+ const buffer = [];
271
+ let total = 0;
272
+ rl.on('line', (line) => {
273
+ if (line.trim() === '')
148
274
  return;
275
+ total++;
276
+ if (maxLines > 0) {
277
+ buffer.push(line);
278
+ if (buffer.length > maxLines)
279
+ buffer.shift();
149
280
  }
150
- try {
151
- // Read the log file using Node.js fs instead of exec
152
- const fs = require('fs');
153
- let logContent = '';
154
- if (fs.existsSync(logPath)) {
155
- // Read the last part of the file (up to 10KB to avoid huge responses)
156
- const stats = fs.statSync(logPath);
157
- const fileSize = stats.size;
158
- const readSize = Math.min(fileSize, 10 * 1024); // 10KB max
159
- const position = Math.max(0, fileSize - readSize);
160
- const buffer = Buffer.alloc(readSize);
161
- const fd = fs.openSync(logPath, 'r');
162
- fs.readSync(fd, buffer, 0, readSize, position);
163
- fs.closeSync(fd);
164
- logContent = buffer.toString('utf8');
165
- }
166
- const logs = logContent.split('\n').filter((line) => line.trim() !== '');
167
- pm2_1.default.disconnect();
168
- res.json({ logs });
169
- }
170
- catch (error) {
171
- console.error(`Error reading log file: ${error}`);
172
- pm2_1.default.disconnect();
173
- res.status(500).json({ error: 'Failed to read log file' });
281
+ else {
282
+ buffer.push(line);
174
283
  }
175
284
  });
285
+ rl.on('close', () => resolve({ lines: buffer, total }));
286
+ rl.on('error', reject);
287
+ inputStream.on('error', reject);
176
288
  });
289
+ };
290
+ // @group LogHistory : Read a specific log file by path — ?lines=N, supports .gz
291
+ // Uses /api/log-file (singular, top-level) to avoid clashing with /api/logs/:id/:type
292
+ app.get('/api/log-file', async (req, res) => {
293
+ const filePath = req.query.path;
294
+ const lines = parseInt(req.query.lines || '200', 10);
295
+ if (!filePath) {
296
+ res.status(400).json({ error: 'path query parameter required' });
297
+ return;
298
+ }
299
+ if (!isAllowedLogPath(filePath)) {
300
+ res.status(403).json({ error: 'Access denied: path is outside PM2 log directories' });
301
+ return;
302
+ }
303
+ try {
304
+ const fs = require('fs');
305
+ const zlib = require('zlib');
306
+ if (!fs.existsSync(filePath)) {
307
+ res.status(404).json({ error: 'File not found' });
308
+ return;
309
+ }
310
+ const inputStream = filePath.endsWith('.gz')
311
+ ? fs.createReadStream(filePath).pipe(zlib.createGunzip())
312
+ : fs.createReadStream(filePath);
313
+ const { lines: result, total } = await streamTailLines(inputStream, lines);
314
+ res.json({ logs: result, totalLines: total });
315
+ }
316
+ catch (err) {
317
+ console.error('Error reading log file:', err);
318
+ res.status(500).json({ error: 'Failed to read log file' });
319
+ }
320
+ });
321
+ // @group LogHistory : Download a specific log file by path (streams .gz as-is)
322
+ app.get('/api/log-file/download', async (req, res) => {
323
+ const filePath = req.query.path;
324
+ if (!filePath) {
325
+ res.status(400).json({ error: 'path query parameter required' });
326
+ return;
327
+ }
328
+ if (!isAllowedLogPath(filePath)) {
329
+ res.status(403).json({ error: 'Access denied' });
330
+ return;
331
+ }
332
+ try {
333
+ const fs = require('fs');
334
+ const zlib = require('zlib');
335
+ const nodePath = require('path');
336
+ if (!fs.existsSync(filePath)) {
337
+ res.status(404).json({ error: 'File not found' });
338
+ return;
339
+ }
340
+ // Decompress .gz server-side so the download is always plain text
341
+ const baseName = nodePath.basename(filePath).replace(/\.gz$/i, '');
342
+ res.setHeader('Content-Disposition', `attachment; filename="${baseName}"`);
343
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
344
+ if (filePath.endsWith('.gz')) {
345
+ fs.createReadStream(filePath).pipe(zlib.createGunzip()).pipe(res);
346
+ }
347
+ else {
348
+ fs.createReadStream(filePath).pipe(res);
349
+ }
350
+ }
351
+ catch (err) {
352
+ console.error('Error downloading log file:', err);
353
+ res.status(500).json({ error: 'Failed to download file' });
354
+ }
177
355
  });
178
356
  // WebSocket for real-time updates
179
357
  io.on('connection', (socket) => {
180
358
  console.log('Client connected');
181
- // Send process updates every 3 seconds
182
- const processInterval = setInterval(() => {
183
- pm2_1.default.connect((err) => {
184
- if (err) {
185
- console.error(err);
186
- return;
187
- }
188
- pm2_1.default.list((err, processList) => {
189
- pm2_1.default.disconnect();
190
- if (err) {
191
- console.error(err);
192
- return;
193
- }
194
- socket.emit('processes', processList);
359
+ // Send process updates every 3 seconds using shared pooled connection
360
+ const processInterval = setInterval(async () => {
361
+ try {
362
+ const processList = await (0, pm2_connection_1.executePM2Command)((callback) => {
363
+ pm2_1.default.list(callback);
195
364
  });
196
- });
365
+ socket.emit('processes', processList);
366
+ }
367
+ catch (err) {
368
+ console.error('Failed to list PM2 processes:', err);
369
+ }
197
370
  }, 3000);
198
371
  // Send system metrics every 2 seconds
199
372
  const metricsInterval = setInterval(() => {
@@ -233,7 +406,7 @@ function createServer() {
233
406
  }
234
407
  // Only start the server if this file is run directly
235
408
  if (require.main === module) {
236
- const PORT = process.env.PORT || 3001;
409
+ const PORT = process.env.PORT || 3101;
237
410
  const HOST = process.env.HOST || 'localhost';
238
411
  const server = createServer();
239
412
  server.listen(PORT, () => {
@@ -11,7 +11,7 @@ const ProjectSetupService_1 = require("../services/ProjectSetupService");
11
11
  const router = (0, express_1.Router)();
12
12
  // Deploy a new application
13
13
  router.post('/', async (req, res) => {
14
- const { name, script, cwd, instances, exec_mode, autorestart, watch, max_memory_restart, env, appType, autoSetup = true } = req.body;
14
+ const { name, script, cwd, namespace, instances, exec_mode, autorestart, watch, max_memory_restart, env, appType, autoSetup = true } = req.body;
15
15
  // Validate required fields
16
16
  if (!name || !script) {
17
17
  return res.status(400).json({ error: 'Name and script path are required' });
@@ -67,6 +67,7 @@ router.post('/', async (req, res) => {
67
67
  name,
68
68
  script,
69
69
  cwd: projectPath,
70
+ namespace: namespace || 'default',
70
71
  instances: parseInt(instances) || 1,
71
72
  exec_mode: exec_mode || 'fork',
72
73
  autorestart: autorestart !== undefined ? autorestart : true,
@@ -158,8 +159,8 @@ router.post('/generate-ecosystem', (req, res) => {
158
159
  env: pm2Env.env || {}
159
160
  };
160
161
  });
161
- const ecosystemConfig = `module.exports = {
162
- apps: ${JSON.stringify(apps, null, 2)}
162
+ const ecosystemConfig = `module.exports = {
163
+ apps: ${JSON.stringify(apps, null, 2)}
163
164
  };`;
164
165
  // Create the file (either at specified path or default location)
165
166
  const filePath = req.body.path || path_1.default.join(process.cwd(), 'ecosystem.config.js');
@@ -212,8 +213,8 @@ router.get('/generate-ecosystem-preview', (req, res) => {
212
213
  env: pm2Env.env || {}
213
214
  };
214
215
  });
215
- const ecosystemConfig = `module.exports = {
216
- apps: ${JSON.stringify(apps, null, 2)}
216
+ const ecosystemConfig = `module.exports = {
217
+ apps: ${JSON.stringify(apps, null, 2)}
217
218
  };`;
218
219
  pm2_1.default.disconnect();
219
220
  res.json({
@@ -38,8 +38,8 @@ const getLogStream = (io, processId, logType) => {
38
38
  tail.stdout.on('data', (data) => {
39
39
  const lines = data.toString().split('\n').filter((line) => line.trim() !== '');
40
40
  lines.forEach((line) => {
41
- // Emit log line to all connected clients
42
- io.emit('log-line', {
41
+ // Emit only to clients subscribed to this specific log stream
42
+ io.to(streamKey).emit('log-line', {
43
43
  processId,
44
44
  logType,
45
45
  line
@@ -70,17 +70,23 @@ const getRemoteLogStream = async (io, connectionId, processId) => {
70
70
  if (!connection || !connection.isConnected()) {
71
71
  throw new Error('Connection not found or not connected');
72
72
  }
73
- // Get process info to find log paths
73
+ // Get process info using the multi-path fallback so pm2 is found regardless of PATH
74
74
  console.log(`Getting process info for: ${processId}`);
75
- const processInfoResult = await connection.executeCommand(`pm2 jlist`, false); // Don't use sudo for listing
75
+ const processInfoResult = await connection.executePM2Command('jlist');
76
76
  if (processInfoResult.code !== 0) {
77
77
  throw new Error(`Failed to get process list: ${processInfoResult.stderr}`);
78
78
  }
79
+ // Extract JSON from output (pm2 jlist may prefix with non-JSON lines)
80
+ let rawOutput = processInfoResult.stdout.trim();
81
+ const startIdx = rawOutput.indexOf('[');
82
+ const endIdx = rawOutput.lastIndexOf(']') + 1;
83
+ if (startIdx !== -1 && endIdx > 0) {
84
+ rawOutput = rawOutput.substring(startIdx, endIdx);
85
+ }
79
86
  let processInfo;
80
87
  try {
81
- const processList = JSON.parse(processInfoResult.stdout);
88
+ const processList = JSON.parse(rawOutput);
82
89
  console.log(`Found ${processList.length} processes`);
83
- // Find the process by ID
84
90
  processInfo = processList.find((proc) => proc.pm_id === parseInt(processId));
85
91
  if (!processInfo) {
86
92
  throw new Error(`Process with ID ${processId} not found`);
@@ -96,10 +102,10 @@ const getRemoteLogStream = async (io, connectionId, processId) => {
96
102
  const streams = {};
97
103
  try {
98
104
  console.log(`Setting up pm2 logs stream for process: ${processName} (ID: ${processId})`);
99
- // Use pm2 logs with --lines 0 --raw to stream only new logs
100
- // Use sudo since PM2 processes are running as root
101
- const pm2LogsCommand = `pm2 logs ${processId} --lines 0 --raw`;
102
- console.log(`About to create pm2 log stream with command: ${pm2LogsCommand} (using sudo)`);
105
+ // Use the resolved pm2 invocation (cached from the jlist call above) so the
106
+ // same shell/path that successfully ran pm2 jlist is reused here.
107
+ const pm2LogsCommand = connection.buildPM2StreamCommand(`logs ${processId} --lines 0 --raw`);
108
+ console.log(`About to create pm2 log stream with command: ${pm2LogsCommand}`);
103
109
  const logStream = await connection.createLogStream(pm2LogsCommand, true);
104
110
  console.log(`Successfully created pm2 logs stream for ${processName}`);
105
111
  logStream.on('data', (data) => {
@@ -154,7 +160,8 @@ const getRemoteLogStream = async (io, connectionId, processId) => {
154
160
  cleanLine = logMatch[1];
155
161
  }
156
162
  console.log(`Emitting remote-log-line for ${connectionId}-${processId} (${logType}):`, cleanLine);
157
- io.emit('remote-log-line', {
163
+ // Emit only to clients subscribed to this specific remote log stream
164
+ io.to(streamKey).emit('remote-log-line', {
158
165
  connectionId,
159
166
  processId,
160
167
  processName,
@@ -165,7 +172,7 @@ const getRemoteLogStream = async (io, connectionId, processId) => {
165
172
  });
166
173
  logStream.on('error', (error) => {
167
174
  console.error(`Error in pm2 logs stream for ${processName}:`, error);
168
- io.emit('remote-log-error', {
175
+ io.to(streamKey).emit('remote-log-error', {
169
176
  connectionId,
170
177
  processId,
171
178
  processName,
@@ -179,7 +186,7 @@ const getRemoteLogStream = async (io, connectionId, processId) => {
179
186
  }
180
187
  catch (error) {
181
188
  console.error(`Failed to create pm2 logs stream for ${processName}:`, error);
182
- io.emit('remote-log-error', {
189
+ io.to(streamKey).emit('remote-log-error', {
183
190
  connectionId,
184
191
  processId,
185
192
  processName,