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
@@ -1,106 +1,126 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  const express_1 = require("express");
4
7
  const child_process_1 = require("child_process");
8
+ const pm2_1 = __importDefault(require("pm2"));
9
+ const pm2_connection_1 = require("../utils/pm2-connection");
5
10
  const router = (0, express_1.Router)();
6
- // List installed modules
7
- router.get('/', (req, res) => {
8
- const pm2Command = (0, child_process_1.spawn)('pm2', ['module:list']);
9
- let output = '';
10
- let errorOutput = '';
11
- pm2Command.stdout.on('data', (data) => {
12
- output += data.toString();
13
- });
14
- pm2Command.stderr.on('data', (data) => {
15
- errorOutput += data.toString();
16
- });
17
- pm2Command.on('close', (code) => {
11
+ // @group Utilities : Resolve pm2 binary path via shell
12
+ const runPM2CLI = (args) => new Promise((resolve, reject) => {
13
+ var _a, _b;
14
+ // Use shell so the OS resolves the global pm2 binary from PATH
15
+ const child = (0, child_process_1.exec)(`pm2 ${args}`);
16
+ let out = '';
17
+ let err = '';
18
+ (_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (d) => { out += d.toString(); });
19
+ (_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (d) => { err += d.toString(); });
20
+ child.on('close', (code) => {
18
21
  if (code !== 0) {
19
- return res.status(500).json({
20
- error: 'Failed to list PM2 modules',
21
- details: errorOutput
22
- });
22
+ reject(new Error(err || `pm2 exited with code ${code}`));
23
+ }
24
+ else {
25
+ resolve({ stdout: out, stderr: err });
23
26
  }
24
- // Parse the output to get module information
27
+ });
28
+ child.on('error', (e) => reject(e));
29
+ });
30
+ // @group APIEndpoints : List installed PM2 modules
31
+ router.get('/', async (_req, res) => {
32
+ try {
33
+ // Use PM2 Node.js API — modules appear as regular processes with module metadata
34
+ const processList = await (0, pm2_connection_1.executePM2Command)((cb) => pm2_1.default.list(cb));
35
+ // PM2 modules have pm2_env.pmx_module === true
36
+ const modules = processList
37
+ .filter((proc) => { var _a; return ((_a = proc.pm2_env) === null || _a === void 0 ? void 0 : _a.pmx_module) === true; })
38
+ .map((proc) => {
39
+ var _a, _b, _c;
40
+ return ({
41
+ name: proc.name,
42
+ version: ((_a = proc.pm2_env) === null || _a === void 0 ? void 0 : _a.version) || ((_b = proc.pm2_env) === null || _b === void 0 ? void 0 : _b.MODULE_VERSION) || 'N/A',
43
+ status: ((_c = proc.pm2_env) === null || _c === void 0 ? void 0 : _c.status) || 'unknown',
44
+ pid: proc.pid,
45
+ pm_id: proc.pm_id,
46
+ });
47
+ });
48
+ res.json(modules);
49
+ }
50
+ catch (error) {
51
+ // Fallback: parse pm2 module:list CLI output
25
52
  try {
26
- const moduleLines = output.split('\n').filter(line => line.includes('│') && !line.includes('Module') && line.trim() !== '');
27
- const modules = moduleLines.map(line => {
53
+ const { stdout } = await runPM2CLI('module:list');
54
+ const moduleLines = stdout
55
+ .split('\n')
56
+ .filter(line => line.includes('│') && !line.includes('Module') && line.trim() !== '');
57
+ const modules = moduleLines
58
+ .map(line => {
28
59
  const parts = line.split('│').map(part => part.trim()).filter(Boolean);
29
60
  if (parts.length >= 3) {
30
- return {
31
- name: parts[0],
32
- version: parts[1],
33
- status: parts[2]
34
- };
61
+ return { name: parts[0], version: parts[1], status: parts[2] };
35
62
  }
36
63
  return null;
37
- }).filter(Boolean);
64
+ })
65
+ .filter(Boolean);
38
66
  res.json(modules);
39
67
  }
40
- catch (error) {
68
+ catch (fallbackError) {
41
69
  res.status(500).json({
42
- error: 'Failed to parse PM2 modules',
43
- details: error
70
+ error: 'Failed to list PM2 modules',
71
+ details: fallbackError.message
44
72
  });
45
73
  }
46
- });
74
+ }
47
75
  });
48
- // Install a module
49
- router.post('/install', (req, res) => {
76
+ // @group APIEndpoints : Install a PM2 module
77
+ router.post('/install', async (req, res) => {
50
78
  const { moduleName } = req.body;
51
79
  if (!moduleName) {
52
80
  return res.status(400).json({ error: 'Module name is required' });
53
81
  }
54
- const pm2Command = (0, child_process_1.spawn)('pm2', ['install', moduleName]);
55
- let output = '';
56
- let errorOutput = '';
57
- pm2Command.stdout.on('data', (data) => {
58
- output += data.toString();
59
- });
60
- pm2Command.stderr.on('data', (data) => {
61
- errorOutput += data.toString();
62
- });
63
- pm2Command.on('close', (code) => {
64
- if (code !== 0) {
65
- return res.status(500).json({
66
- error: `Failed to install module: ${moduleName}`,
67
- details: errorOutput
68
- });
69
- }
82
+ // Basic validation only allow alphanumeric, @, /, -, .
83
+ if (!/^[@a-zA-Z0-9/_\-.]+$/.test(moduleName)) {
84
+ return res.status(400).json({ error: 'Invalid module name' });
85
+ }
86
+ try {
87
+ const { stdout } = await runPM2CLI(`install ${moduleName}`);
70
88
  res.json({
71
89
  success: true,
72
90
  message: `Successfully installed module: ${moduleName}`,
73
- details: output
91
+ details: stdout
74
92
  });
75
- });
93
+ }
94
+ catch (error) {
95
+ res.status(500).json({
96
+ error: `Failed to install module: ${moduleName}`,
97
+ details: error.message
98
+ });
99
+ }
76
100
  });
77
- // Uninstall a module
78
- router.delete('/:moduleName', (req, res) => {
101
+ // @group APIEndpoints : Uninstall a PM2 module
102
+ router.delete('/:moduleName', async (req, res) => {
79
103
  const { moduleName } = req.params;
80
104
  if (!moduleName) {
81
105
  return res.status(400).json({ error: 'Module name is required' });
82
106
  }
83
- const pm2Command = (0, child_process_1.spawn)('pm2', ['uninstall', moduleName]);
84
- let output = '';
85
- let errorOutput = '';
86
- pm2Command.stdout.on('data', (data) => {
87
- output += data.toString();
88
- });
89
- pm2Command.stderr.on('data', (data) => {
90
- errorOutput += data.toString();
91
- });
92
- pm2Command.on('close', (code) => {
93
- if (code !== 0) {
94
- return res.status(500).json({
95
- error: `Failed to uninstall module: ${moduleName}`,
96
- details: errorOutput
97
- });
98
- }
107
+ // Basic validation
108
+ if (!/^[@a-zA-Z0-9/_\-.]+$/.test(moduleName)) {
109
+ return res.status(400).json({ error: 'Invalid module name' });
110
+ }
111
+ try {
112
+ const { stdout } = await runPM2CLI(`uninstall ${moduleName}`);
99
113
  res.json({
100
114
  success: true,
101
115
  message: `Successfully uninstalled module: ${moduleName}`,
102
- details: output
116
+ details: stdout
103
117
  });
104
- });
118
+ }
119
+ catch (error) {
120
+ res.status(500).json({
121
+ error: `Failed to uninstall module: ${moduleName}`,
122
+ details: error.message
123
+ });
124
+ }
105
125
  });
106
126
  exports.default = router;
@@ -6,6 +6,26 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const express_1 = __importDefault(require("express"));
7
7
  const remote_connection_1 = require("../utils/remote-connection");
8
8
  const router = express_1.default.Router();
9
+ // @group Security : Validate remote log file paths before shell interpolation
10
+ const SHELL_UNSAFE_CHARS = /['"`;$|&<>(){}\\\n\r\0]/;
11
+ const MAX_LOG_LINES = 10000;
12
+ const validateRemotePath = (filePath) => {
13
+ if (!filePath)
14
+ return false;
15
+ if (filePath.includes('..'))
16
+ return false;
17
+ if (SHELL_UNSAFE_CHARS.test(filePath))
18
+ return false;
19
+ if (!/\.(log|gz)$/i.test(filePath))
20
+ return false;
21
+ return true;
22
+ };
23
+ const safeLogLines = (raw) => {
24
+ const n = parseInt(raw || '200', 10);
25
+ if (!Number.isFinite(n) || n < 0)
26
+ return 200;
27
+ return Math.min(n, MAX_LOG_LINES);
28
+ };
9
29
  /**
10
30
  * Connect to an existing remote server
11
31
  * POST /api/remote/:connectionId/connect
@@ -20,14 +40,11 @@ router.post('/:connectionId/connect', async (req, res) => {
20
40
  error: 'Connection not found'
21
41
  });
22
42
  }
23
- // Connect to the remote server
43
+ // Establish the SSH connection only — PM2 detection happens lazily
44
+ // when processes are first requested, avoiding a slow multi-command
45
+ // pre-check that causes browser timeouts on the connect button.
24
46
  await connection.connect();
25
- // Check if PM2 is installed
26
- const isPM2Installed = await connection.checkPM2Installation();
27
- res.json({
28
- success: true,
29
- isPM2Installed
30
- });
47
+ res.json({ success: true });
31
48
  }
32
49
  catch (error) {
33
50
  console.error('Connection error:', error);
@@ -481,29 +498,36 @@ router.get('/:connectionId/logs/:processId', async (req, res) => {
481
498
  success: false,
482
499
  error: 'Connection not established'
483
500
  });
484
- } // Get log paths from PM2 process info
485
- const processInfoResult = await connection.executeCommand(`pm2 show ${processId} --json`);
501
+ }
502
+ // Get log paths from PM2 process info — use the PATH-fallback executor so
503
+ // pm2 is found regardless of the remote shell environment (nvm, npm-global, etc.)
504
+ const processInfoResult = await connection.executePM2Command('jlist');
486
505
  if (processInfoResult.code !== 0) {
487
506
  return res.status(500).json({
488
507
  success: false,
489
- error: 'Failed to get process info'
508
+ error: 'Failed to get PM2 process list'
490
509
  });
491
510
  }
492
511
  let processInfo;
493
512
  try {
494
- processInfo = JSON.parse(processInfoResult.stdout);
495
- if (!Array.isArray(processInfo) || processInfo.length === 0) {
513
+ let raw = processInfoResult.stdout.trim();
514
+ const start = raw.indexOf('[');
515
+ const end = raw.lastIndexOf(']') + 1;
516
+ if (start !== -1 && end > 0)
517
+ raw = raw.substring(start, end);
518
+ const processList = JSON.parse(raw);
519
+ processInfo = processList.find((p) => p.pm_id === parseInt(processId, 10) || p.name === processId);
520
+ if (!processInfo) {
496
521
  return res.status(404).json({
497
522
  success: false,
498
523
  error: 'Process not found'
499
524
  });
500
525
  }
501
- processInfo = processInfo[0];
502
526
  }
503
527
  catch (parseError) {
504
528
  return res.status(500).json({
505
529
  success: false,
506
- error: 'Failed to parse process info'
530
+ error: 'Failed to parse PM2 process list'
507
531
  });
508
532
  }
509
533
  const outLogPath = (_a = processInfo.pm2_env) === null || _a === void 0 ? void 0 : _a.pm_out_log_path;
@@ -548,6 +572,246 @@ router.get('/:connectionId/logs/:processId', async (req, res) => {
548
572
  });
549
573
  }
550
574
  });
575
+ // @group LogHistory : Helper — resolve log path for a given process on a remote connection
576
+ const resolveRemoteLogPath = async (connection, processId, logType) => {
577
+ var _a, _b;
578
+ const processInfoResult = await connection.executePM2Command('jlist');
579
+ if (processInfoResult.code !== 0)
580
+ return { logPath: null, error: 'Failed to get PM2 process list' };
581
+ try {
582
+ let raw = processInfoResult.stdout.trim();
583
+ const start = raw.indexOf('[');
584
+ const end = raw.lastIndexOf(']') + 1;
585
+ if (start !== -1 && end > 0)
586
+ raw = raw.substring(start, end);
587
+ const processList = JSON.parse(raw);
588
+ const proc = processList.find((p) => p.pm_id === parseInt(processId, 10) || p.name === processId);
589
+ if (!proc)
590
+ return { logPath: null, error: 'Process not found' };
591
+ const key = logType === 'out' ? 'pm_out_log_path' : 'pm_err_log_path';
592
+ return { logPath: (_b = (_a = proc.pm2_env) === null || _a === void 0 ? void 0 : _a[key]) !== null && _b !== void 0 ? _b : null };
593
+ }
594
+ catch {
595
+ return { logPath: null, error: 'Failed to parse PM2 process list' };
596
+ }
597
+ };
598
+ /**
599
+ * Get log lines from a specific log type on a remote process — ?lines=N (default 200, 0 = all)
600
+ * GET /api/remote/:connectionId/logs/:processId/:type
601
+ */
602
+ router.get('/:connectionId/logs/:processId/:type', async (req, res) => {
603
+ var _a;
604
+ try {
605
+ const { connectionId, processId, type } = req.params;
606
+ const logType = type === 'err' ? 'err' : 'out';
607
+ const lines = safeLogLines(req.query.lines);
608
+ const connection = remote_connection_1.remoteConnectionManager.getConnection(connectionId);
609
+ if (!connection)
610
+ return res.status(404).json({ success: false, error: 'Connection not found' });
611
+ if (!connection.isConnected())
612
+ return res.status(400).json({ success: false, error: 'Not connected' });
613
+ const { logPath, error } = await resolveRemoteLogPath(connection, processId, logType);
614
+ if (!logPath)
615
+ return res.status(404).json({ success: false, error: error || 'Log path not found' });
616
+ // tail -n 0 = all lines; use wc -l to get total count alongside
617
+ const lineArg = lines === 0 ? '+1' : `-${lines}`;
618
+ const cmd = `{ wc -l < "${logPath}" 2>/dev/null || echo 0; } && tail -n ${lineArg} "${logPath}" 2>/dev/null`;
619
+ let result = await connection.executeCommand(cmd);
620
+ // Fallback to sudo if the file is unreadable (root-owned logs)
621
+ if (result.code !== 0 || !result.stdout.trim()) {
622
+ result = await connection.executeCommand(`{ sudo wc -l < "${logPath}" 2>/dev/null || echo 0; } && sudo tail -n ${lineArg} "${logPath}" 2>/dev/null`);
623
+ }
624
+ const outputLines = result.stdout.split('\n');
625
+ const totalLines = parseInt(((_a = outputLines[0]) === null || _a === void 0 ? void 0 : _a.trim()) || '0', 10);
626
+ const logLines = outputLines.slice(1).filter((l) => l.trim() !== '');
627
+ res.json({ logs: logLines, logPath, totalLines });
628
+ }
629
+ catch (error) {
630
+ console.error('Error fetching remote log history:', error);
631
+ res.status(500).json({ success: false, error: `Server error: ${error instanceof Error ? error.message : 'Unknown error'}` });
632
+ }
633
+ });
634
+ /**
635
+ * Download full log file from a remote process via SSH
636
+ * GET /api/remote/:connectionId/logs/:processId/:type/download
637
+ */
638
+ router.get('/:connectionId/logs/:processId/:type/download', async (req, res) => {
639
+ try {
640
+ const { connectionId, processId, type } = req.params;
641
+ const logType = type === 'err' ? 'err' : 'out';
642
+ const connection = remote_connection_1.remoteConnectionManager.getConnection(connectionId);
643
+ if (!connection)
644
+ return res.status(404).json({ success: false, error: 'Connection not found' });
645
+ if (!connection.isConnected())
646
+ return res.status(400).json({ success: false, error: 'Not connected' });
647
+ const { logPath, error } = await resolveRemoteLogPath(connection, processId, logType);
648
+ if (!logPath)
649
+ return res.status(404).json({ success: false, error: error || 'Log path not found' });
650
+ const fileName = `${processId}-${logType}.log`;
651
+ await connection.streamFileToResponse(logPath, res, fileName);
652
+ }
653
+ catch (error) {
654
+ console.error('Error downloading remote log:', error);
655
+ if (!res.headersSent) {
656
+ res.status(500).json({ success: false, error: `Server error: ${error instanceof Error ? error.message : 'Unknown error'}` });
657
+ }
658
+ }
659
+ });
660
+ /**
661
+ * List all log files (current + rotated) for a remote process
662
+ * GET /api/remote/:connectionId/log-files/:processId
663
+ * Uses /log-files/ prefix to avoid Express matching /:processId/:type with type='files'
664
+ */
665
+ router.get('/:connectionId/log-files/:processId', async (req, res) => {
666
+ try {
667
+ const { connectionId, processId } = req.params;
668
+ const connection = remote_connection_1.remoteConnectionManager.getConnection(connectionId);
669
+ if (!connection)
670
+ return res.status(404).json({ success: false, error: 'Connection not found' });
671
+ if (!connection.isConnected())
672
+ return res.status(400).json({ success: false, error: 'Not connected' });
673
+ // Resolve both log paths so we know the directory and base name
674
+ const { logPath: outPath } = await resolveRemoteLogPath(connection, processId, 'out');
675
+ const { logPath: errPath } = await resolveRemoteLogPath(connection, processId, 'err');
676
+ if (!outPath && !errPath) {
677
+ return res.status(404).json({ success: false, error: 'No log paths found for this process' });
678
+ }
679
+ // Derive log directory + base name from the out log path (or err if out missing)
680
+ const refPath = outPath || errPath;
681
+ const logDir = refPath.substring(0, refPath.lastIndexOf('/'));
682
+ const baseName = refPath.split('/').pop().replace(/-out\.log.*$/, '').replace(/-(error|err)\.log.*$/, '');
683
+ // List matching files in the log directory
684
+ const lsCmd = `ls -la "${logDir}" 2>/dev/null`;
685
+ let lsResult = await connection.executeCommand(lsCmd);
686
+ if (lsResult.code !== 0)
687
+ lsResult = await connection.executeCommand(`sudo ${lsCmd}`);
688
+ const files = [];
689
+ if (lsResult.code === 0) {
690
+ for (const line of lsResult.stdout.split('\n')) {
691
+ // ls -la line: permissions links owner group size month day time name
692
+ const parts = line.trim().split(/\s+/);
693
+ if (parts.length < 9)
694
+ continue;
695
+ const fileName = parts.slice(8).join(' ');
696
+ if (!fileName.startsWith(baseName))
697
+ continue;
698
+ const size = parseInt(parts[4], 10) || 0;
699
+ const month = parts[5];
700
+ const day = parts[6];
701
+ const timeOrYr = parts[7];
702
+ const modified = `${month} ${day} ${timeOrYr}`;
703
+ let type = 'unknown';
704
+ if (fileName.includes('-out'))
705
+ type = 'out';
706
+ else if (fileName.includes('-error') || fileName.includes('-err'))
707
+ type = 'err';
708
+ files.push({
709
+ name: fileName,
710
+ path: `${logDir}/${fileName}`,
711
+ size,
712
+ modified,
713
+ type,
714
+ compressed: fileName.endsWith('.gz'),
715
+ });
716
+ }
717
+ }
718
+ // Sort: current files first (no date suffix), then rotated newest first
719
+ files.sort((a, b) => {
720
+ const aRot = a.name.includes('__') || /\d{4}-\d{2}/.test(a.name);
721
+ const bRot = b.name.includes('__') || /\d{4}-\d{2}/.test(b.name);
722
+ if (!aRot && bRot)
723
+ return -1;
724
+ if (aRot && !bRot)
725
+ return 1;
726
+ return b.name.localeCompare(a.name);
727
+ });
728
+ res.json({ files });
729
+ }
730
+ catch (error) {
731
+ console.error('Error listing remote log files:', error);
732
+ res.status(500).json({ success: false, error: `Server error: ${error instanceof Error ? error.message : 'Unknown error'}` });
733
+ }
734
+ });
735
+ /**
736
+ * Read a specific remote log file by path — ?lines=N, handles .gz via zcat
737
+ * GET /api/remote/:connectionId/log-file?path=...&lines=N
738
+ * Uses /log-file top-level to avoid clashing with /logs/:processId routes
739
+ */
740
+ router.get('/:connectionId/log-file', async (req, res) => {
741
+ var _a;
742
+ try {
743
+ const { connectionId } = req.params;
744
+ const filePath = req.query.path;
745
+ const lines = safeLogLines(req.query.lines);
746
+ if (!filePath)
747
+ return res.status(400).json({ error: 'path query parameter required' });
748
+ // Strict path validation — blocks traversal, shell metacharacters, and non-log extensions
749
+ if (!validateRemotePath(filePath)) {
750
+ return res.status(403).json({ error: 'Access denied: invalid or unsafe log file path' });
751
+ }
752
+ const connection = remote_connection_1.remoteConnectionManager.getConnection(connectionId);
753
+ if (!connection)
754
+ return res.status(404).json({ success: false, error: 'Connection not found' });
755
+ if (!connection.isConnected())
756
+ return res.status(400).json({ success: false, error: 'Not connected' });
757
+ const isGz = filePath.endsWith('.gz');
758
+ const catCmd = isGz ? `zcat "${filePath}" 2>/dev/null` : `cat "${filePath}" 2>/dev/null`;
759
+ const lineArg = lines === 0 ? '' : `| tail -n ${lines}`;
760
+ const countCmd = isGz
761
+ ? `zcat "${filePath}" 2>/dev/null | wc -l`
762
+ : `wc -l < "${filePath}" 2>/dev/null`;
763
+ const [contentResult, countResult] = await Promise.all([
764
+ connection.executeCommand(`${catCmd} ${lineArg}`).catch(() => ({ code: 1, stdout: '', stderr: '' })),
765
+ connection.executeCommand(countCmd).catch(() => ({ code: 1, stdout: '0', stderr: '' })),
766
+ ]);
767
+ // Fallback to sudo on permission error
768
+ const content = contentResult.code === 0 && contentResult.stdout.trim()
769
+ ? contentResult
770
+ : await connection.executeCommand(`sudo ${catCmd} ${lineArg}`);
771
+ const totalLines = parseInt(((_a = countResult.stdout) === null || _a === void 0 ? void 0 : _a.trim()) || '0', 10);
772
+ const logLines = (content.stdout || '').split('\n').filter((l) => l.trim() !== '');
773
+ res.json({ logs: logLines, totalLines });
774
+ }
775
+ catch (error) {
776
+ console.error('Error reading remote log file:', error);
777
+ res.status(500).json({ success: false, error: `Server error: ${error instanceof Error ? error.message : 'Unknown error'}` });
778
+ }
779
+ });
780
+ /**
781
+ * Download a specific remote log file by path via SSH cat/zcat
782
+ * GET /api/remote/:connectionId/log-file/download?path=...
783
+ */
784
+ router.get('/:connectionId/log-file/download', async (req, res) => {
785
+ try {
786
+ const { connectionId } = req.params;
787
+ const filePath = req.query.path;
788
+ if (!filePath)
789
+ return res.status(400).json({ error: 'path query parameter required' });
790
+ if (!validateRemotePath(filePath)) {
791
+ return res.status(403).json({ error: 'Access denied: invalid or unsafe log file path' });
792
+ }
793
+ const connection = remote_connection_1.remoteConnectionManager.getConnection(connectionId);
794
+ if (!connection)
795
+ return res.status(404).json({ success: false, error: 'Connection not found' });
796
+ if (!connection.isConnected())
797
+ return res.status(400).json({ success: false, error: 'Not connected' });
798
+ // .gz files: stream via SFTP + local gunzip (no server-side memory buffering)
799
+ if (filePath.endsWith('.gz')) {
800
+ const gzName = filePath.split('/').pop().replace(/\.gz$/, '');
801
+ await connection.streamGzFileToResponse(filePath, res, gzName);
802
+ return;
803
+ }
804
+ // Plain files: SFTP stream
805
+ const fileName = filePath.split('/').pop();
806
+ await connection.streamFileToResponse(filePath, res, fileName);
807
+ }
808
+ catch (error) {
809
+ console.error('Error downloading remote log file:', error);
810
+ if (!res.headersSent) {
811
+ res.status(500).json({ success: false, error: `Server error: ${error instanceof Error ? error.message : 'Unknown error'}` });
812
+ }
813
+ }
814
+ });
551
815
  /**
552
816
  * Get list of all remote connections with their status
553
817
  * GET /api/remote/connections
@@ -561,7 +825,7 @@ router.get('/connections', async (req, res) => {
561
825
  host: conn.host,
562
826
  port: conn.port,
563
827
  username: conn.username,
564
- isConnected: conn.isConnected(),
828
+ connected: conn.isConnected(),
565
829
  isPM2Installed: conn.isPM2Installed
566
830
  }));
567
831
  res.json(connectionsList);
@@ -711,29 +975,4 @@ router.post('/:connectionId/install-pm2', async (req, res) => {
711
975
  });
712
976
  }
713
977
  });
714
- /**
715
- * Delete a connection configuration
716
- * DELETE /api/remote/connections/:connectionId
717
- */
718
- router.delete('/connections/:connectionId', async (req, res) => {
719
- try {
720
- const { connectionId } = req.params;
721
- // Disconnect if connected
722
- await remote_connection_1.remoteConnectionManager.closeConnection(connectionId);
723
- // Delete the connection from the manager
724
- const success = remote_connection_1.remoteConnectionManager.deleteConnection(connectionId);
725
- if (success) {
726
- res.json({ success: true, message: 'Connection deleted' });
727
- }
728
- else {
729
- res.status(404).json({ success: false, error: 'Connection not found' });
730
- }
731
- }
732
- catch (error) {
733
- res.status(500).json({
734
- success: false,
735
- error: `Server error: ${error.message}`
736
- });
737
- }
738
- });
739
978
  exports.default = router;
@@ -0,0 +1,3 @@
1
+ import { Router } from 'express';
2
+ declare const router: Router;
3
+ export default router;