ezpm2gui 1.8.0 → 1.9.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 (31) hide show
  1. package/README.md +355 -330
  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/server/config/project-configs.json +235 -235
  7. package/dist/server/index.js +4 -4
  8. package/dist/server/routes/deployApplication.js +4 -4
  9. package/dist/server/routes/logStreaming.js +35 -4
  10. package/dist/server/routes/remoteConnections.js +44 -12
  11. package/dist/server/utils/encryption.d.ts +22 -0
  12. package/dist/server/utils/encryption.js +53 -0
  13. package/dist/server/utils/remote-connection.js +3 -3
  14. package/dist/server/utils/remote-metrics-db.js +59 -59
  15. package/package.json +86 -86
  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.2836d066.css +5 -0
  24. package/src/client/build/static/css/main.2836d066.css.map +1 -0
  25. package/src/client/build/static/js/{main.28a4a583.js → main.d5c19622.js} +3 -3
  26. package/src/client/build/static/js/{main.28a4a583.js.map → main.d5c19622.js.map} +1 -1
  27. package/dist/server/config/cron-jobs.json +0 -1
  28. package/dist/server/config/remote-connections.json +0 -3
  29. package/src/client/build/static/css/main.9decb204.css +0 -5
  30. package/src/client/build/static/css/main.9decb204.css.map +0 -1
  31. /package/src/client/build/static/js/{main.28a4a583.js.LICENSE.txt → main.d5c19622.js.LICENSE.txt} +0 -0
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  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
+ const encryption_1 = require("../utils/encryption");
8
9
  const router = express_1.default.Router();
9
10
  // @group Security : Validate remote log file paths before shell interpolation
10
11
  const SHELL_UNSAFE_CHARS = /['"`;$|&<>(){}\\\n\r\0]/;
@@ -26,6 +27,29 @@ const safeLogLines = (raw) => {
26
27
  return 200;
27
28
  return Math.min(n, MAX_LOG_LINES);
28
29
  };
30
+ /**
31
+ * Decrypts a field that may be either a plain string (legacy/internal)
32
+ * or an EncryptedPayload object produced by the client-side hybrid encryptor.
33
+ */
34
+ function decryptField(value) {
35
+ if (!value)
36
+ return undefined;
37
+ if (typeof value === 'string')
38
+ return value;
39
+ try {
40
+ return (0, encryption_1.decryptTransitPayload)(value);
41
+ }
42
+ catch {
43
+ throw new Error('Failed to decrypt field — possible tampering or key mismatch');
44
+ }
45
+ }
46
+ /**
47
+ * Expose RSA public key so the client can encrypt sensitive fields before sending.
48
+ * GET /api/remote/public-key
49
+ */
50
+ router.get('/public-key', (_req, res) => {
51
+ res.json({ publicKey: (0, encryption_1.getRSAPublicKey)() });
52
+ });
29
53
  /**
30
54
  * Connect to an existing remote server
31
55
  * POST /api/remote/:connectionId/connect
@@ -848,17 +872,21 @@ router.get('/connections', async (req, res) => {
848
872
  */
849
873
  router.post('/connections', (req, res) => {
850
874
  try {
851
- const connectionConfig = req.body;
852
- if (!connectionConfig.name || !connectionConfig.host || !connectionConfig.username) {
875
+ const body = req.body;
876
+ if (!body.name || !body.host || !body.username) {
853
877
  return res.status(400).json({
854
878
  success: false,
855
879
  error: 'Missing required connection parameters: name, host, or username'
856
880
  });
857
881
  }
858
- // Ensure port is set
859
- if (!connectionConfig.port) {
860
- connectionConfig.port = 22;
861
- }
882
+ // Decrypt sensitive fields that may have been encrypted by the client
883
+ const connectionConfig = {
884
+ ...body,
885
+ port: body.port || 22,
886
+ password: decryptField(body.password),
887
+ privateKey: decryptField(body.privateKey),
888
+ passphrase: decryptField(body.passphrase),
889
+ };
862
890
  // Create the connection
863
891
  const connectionId = remote_connection_1.remoteConnectionManager.createConnection(connectionConfig);
864
892
  res.status(201).json({
@@ -881,17 +909,21 @@ router.post('/connections', (req, res) => {
881
909
  router.put('/connections/:connectionId', async (req, res) => {
882
910
  try {
883
911
  const { connectionId } = req.params;
884
- const connectionConfig = req.body;
885
- if (!connectionConfig.name || !connectionConfig.host || !connectionConfig.username) {
912
+ const body = req.body;
913
+ if (!body.name || !body.host || !body.username) {
886
914
  return res.status(400).json({
887
915
  success: false,
888
916
  error: 'Missing required connection parameters: name, host, or username'
889
917
  });
890
918
  }
891
- // Ensure port is set
892
- if (!connectionConfig.port) {
893
- connectionConfig.port = 22;
894
- }
919
+ // Decrypt sensitive fields that may have been encrypted by the client
920
+ const connectionConfig = {
921
+ ...body,
922
+ port: body.port || 22,
923
+ password: decryptField(body.password),
924
+ privateKey: decryptField(body.privateKey),
925
+ passphrase: decryptField(body.passphrase),
926
+ };
895
927
  // Update the connection
896
928
  const success = await remote_connection_1.remoteConnectionManager.updateConnection(connectionId, connectionConfig);
897
929
  if (!success) {
@@ -10,3 +10,25 @@ export declare function encrypt(text: string): string;
10
10
  * @returns The decrypted text
11
11
  */
12
12
  export declare function decrypt(encryptedText: string): string;
13
+ /**
14
+ * Returns the server's RSA public key in PEM (SPKI) format.
15
+ * Expose this via a GET endpoint so clients can encrypt before sending.
16
+ */
17
+ export declare function getRSAPublicKey(): string;
18
+ /**
19
+ * Hybrid-encrypted payload sent by the client.
20
+ * - encryptedKey : RSA-OAEP encrypted 256-bit AES key (base64)
21
+ * - iv : 12-byte AES-GCM IV (base64)
22
+ * - data : AES-256-GCM ciphertext + 16-byte auth-tag (base64)
23
+ */
24
+ export interface EncryptedPayload {
25
+ encryptedKey: string;
26
+ iv: string;
27
+ data: string;
28
+ }
29
+ /**
30
+ * Decrypts a hybrid-encrypted payload produced by the client.
31
+ * 1. Unwraps the AES key with RSA-OAEP (SHA-256)
32
+ * 2. Decrypts the data with AES-256-GCM
33
+ */
34
+ export declare function decryptTransitPayload(payload: EncryptedPayload): string;
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.encrypt = encrypt;
7
7
  exports.decrypt = decrypt;
8
+ exports.getRSAPublicKey = getRSAPublicKey;
9
+ exports.decryptTransitPayload = decryptTransitPayload;
8
10
  /**
9
11
  * Utility for encrypting and decrypting sensitive data
10
12
  */
@@ -58,3 +60,54 @@ function decrypt(encryptedText) {
58
60
  return '';
59
61
  }
60
62
  }
63
+ // ---------------------------------------------------------------------------
64
+ // RSA-OAEP key pair for in-transit encryption
65
+ // A fresh key pair is generated once per server process and kept in memory.
66
+ // The public key is shared with clients so they can encrypt sensitive fields
67
+ // before sending them over the network. The private key never leaves the server.
68
+ // ---------------------------------------------------------------------------
69
+ let _rsaPrivateKey = null;
70
+ let _rsaPublicKeyPem = null;
71
+ function getOrCreateRSAKeyPair() {
72
+ if (!_rsaPrivateKey || !_rsaPublicKeyPem) {
73
+ const { privateKey, publicKey } = crypto_1.default.generateKeyPairSync('rsa', {
74
+ modulusLength: 2048,
75
+ });
76
+ _rsaPrivateKey = privateKey;
77
+ _rsaPublicKeyPem = publicKey.export({ type: 'spki', format: 'pem' });
78
+ }
79
+ return { publicKeyPem: _rsaPublicKeyPem, privateKey: _rsaPrivateKey };
80
+ }
81
+ /**
82
+ * Returns the server's RSA public key in PEM (SPKI) format.
83
+ * Expose this via a GET endpoint so clients can encrypt before sending.
84
+ */
85
+ function getRSAPublicKey() {
86
+ return getOrCreateRSAKeyPair().publicKeyPem;
87
+ }
88
+ /**
89
+ * Decrypts a hybrid-encrypted payload produced by the client.
90
+ * 1. Unwraps the AES key with RSA-OAEP (SHA-256)
91
+ * 2. Decrypts the data with AES-256-GCM
92
+ */
93
+ function decryptTransitPayload(payload) {
94
+ const { privateKey } = getOrCreateRSAKeyPair();
95
+ // Step 1 — decrypt the AES-256 key with RSA-OAEP / SHA-256
96
+ const encryptedAesKey = Buffer.from(payload.encryptedKey, 'base64');
97
+ const aesKey = crypto_1.default.privateDecrypt({
98
+ key: privateKey,
99
+ padding: crypto_1.default.constants.RSA_PKCS1_OAEP_PADDING,
100
+ oaepHash: 'sha256',
101
+ }, encryptedAesKey);
102
+ // Step 2 — decrypt data with AES-256-GCM
103
+ // WebCrypto appends the 16-byte auth tag at the end of the ciphertext buffer
104
+ const iv = Buffer.from(payload.iv, 'base64');
105
+ const encryptedBuf = Buffer.from(payload.data, 'base64');
106
+ const authTag = encryptedBuf.slice(encryptedBuf.length - 16);
107
+ const ciphertext = encryptedBuf.slice(0, encryptedBuf.length - 16);
108
+ const decipher = crypto_1.default.createDecipheriv('aes-256-gcm', aesKey, iv);
109
+ decipher.setAuthTag(authTag);
110
+ let decrypted = decipher.update(ciphertext, undefined, 'utf8');
111
+ decrypted += decipher.final('utf8');
112
+ return decrypted;
113
+ }
@@ -78,9 +78,9 @@ class RemoteConnection extends events_1.EventEmitter {
78
78
  host: this.config.host,
79
79
  port: this.config.port || 22,
80
80
  username: this.config.username,
81
- // Add reasonable timeouts
82
- readyTimeout: 10000,
83
- keepaliveInterval: 30000
81
+ // Add reasonable timeouts optimized for VPN/remote scenarios
82
+ readyTimeout: 20000, // 20 seconds to establish connection (was 10s)
83
+ keepaliveInterval: 10000 // Send keepalive every 10 seconds (was 30s) for aggressive firewalls
84
84
  };
85
85
  // Debug output
86
86
  console.log(`Connecting to ${this.config.host}:${this.config.port} as ${this.config.username}`);
@@ -22,24 +22,24 @@ class RemoteMetricsDB {
22
22
  }
23
23
  // @group RemoteMetricsDB : Create tables and indexes on first run
24
24
  initSchema() {
25
- this.db.exec(`
26
- CREATE TABLE IF NOT EXISTS remote_metrics (
27
- id INTEGER PRIMARY KEY AUTOINCREMENT,
28
- connection_id TEXT NOT NULL,
29
- connection_name TEXT NOT NULL,
30
- process_name TEXT NOT NULL,
31
- pm_id INTEGER NOT NULL,
32
- timestamp INTEGER NOT NULL,
33
- cpu REAL NOT NULL,
34
- memory_bytes INTEGER NOT NULL,
35
- memory_mb REAL NOT NULL
36
- );
37
-
38
- CREATE INDEX IF NOT EXISTS idx_rmet_conn_proc_ts
39
- ON remote_metrics (connection_id, process_name, timestamp);
40
-
41
- CREATE INDEX IF NOT EXISTS idx_rmet_conn_ts
42
- ON remote_metrics (connection_id, timestamp);
25
+ this.db.exec(`
26
+ CREATE TABLE IF NOT EXISTS remote_metrics (
27
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
28
+ connection_id TEXT NOT NULL,
29
+ connection_name TEXT NOT NULL,
30
+ process_name TEXT NOT NULL,
31
+ pm_id INTEGER NOT NULL,
32
+ timestamp INTEGER NOT NULL,
33
+ cpu REAL NOT NULL,
34
+ memory_bytes INTEGER NOT NULL,
35
+ memory_mb REAL NOT NULL
36
+ );
37
+
38
+ CREATE INDEX IF NOT EXISTS idx_rmet_conn_proc_ts
39
+ ON remote_metrics (connection_id, process_name, timestamp);
40
+
41
+ CREATE INDEX IF NOT EXISTS idx_rmet_conn_ts
42
+ ON remote_metrics (connection_id, timestamp);
43
43
  `);
44
44
  }
45
45
  // @group RemoteMetricsDB : Delete records older than 30 days to keep DB lean
@@ -49,18 +49,18 @@ class RemoteMetricsDB {
49
49
  }
50
50
  // @group RemoteMetricsDB : Persist a single data-point
51
51
  insert(row) {
52
- this.db.prepare(`
53
- INSERT INTO remote_metrics
54
- (connection_id, connection_name, process_name, pm_id, timestamp, cpu, memory_bytes, memory_mb)
55
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
52
+ this.db.prepare(`
53
+ INSERT INTO remote_metrics
54
+ (connection_id, connection_name, process_name, pm_id, timestamp, cpu, memory_bytes, memory_mb)
55
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
56
56
  `).run(row.connection_id, row.connection_name, row.process_name, row.pm_id, row.timestamp, row.cpu, row.memory_bytes, row.memory_mb);
57
57
  }
58
58
  // @group RemoteMetricsDB : Bulk-insert multiple data-points in one transaction (efficient)
59
59
  insertBatch(rows) {
60
- const stmt = this.db.prepare(`
61
- INSERT INTO remote_metrics
62
- (connection_id, connection_name, process_name, pm_id, timestamp, cpu, memory_bytes, memory_mb)
63
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
60
+ const stmt = this.db.prepare(`
61
+ INSERT INTO remote_metrics
62
+ (connection_id, connection_name, process_name, pm_id, timestamp, cpu, memory_bytes, memory_mb)
63
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
64
64
  `);
65
65
  const tx = this.db.transaction(() => {
66
66
  for (const row of rows) {
@@ -71,59 +71,59 @@ class RemoteMetricsDB {
71
71
  }
72
72
  // @group RemoteMetricsDB : Return all (connection_id, connection_name) pairs that have data
73
73
  getConnectionsWithData() {
74
- return this.db.prepare(`
75
- SELECT DISTINCT connection_id AS id, connection_name AS name
76
- FROM remote_metrics
77
- ORDER BY name
74
+ return this.db.prepare(`
75
+ SELECT DISTINCT connection_id AS id, connection_name AS name
76
+ FROM remote_metrics
77
+ ORDER BY name
78
78
  `).all();
79
79
  }
80
80
  // @group RemoteMetricsDB : Return distinct process names recorded for a connection
81
81
  getProcessNames(connectionId) {
82
- const rows = this.db.prepare(`
83
- SELECT DISTINCT process_name
84
- FROM remote_metrics
85
- WHERE connection_id = ?
86
- ORDER BY process_name
82
+ const rows = this.db.prepare(`
83
+ SELECT DISTINCT process_name
84
+ FROM remote_metrics
85
+ WHERE connection_id = ?
86
+ ORDER BY process_name
87
87
  `).all(connectionId);
88
88
  return rows.map(r => r.process_name);
89
89
  }
90
90
  // @group RemoteMetricsDB : Return metric rows for a specific process within a time range
91
91
  getMetrics(connectionId, processName, from, to, maxPoints = 500) {
92
92
  // Bucket-reduce to avoid sending thousands of points for long ranges
93
- const total = this.db.prepare(`
94
- SELECT COUNT(*) AS cnt FROM remote_metrics
95
- WHERE connection_id = ? AND process_name = ? AND timestamp BETWEEN ? AND ?
93
+ const total = this.db.prepare(`
94
+ SELECT COUNT(*) AS cnt FROM remote_metrics
95
+ WHERE connection_id = ? AND process_name = ? AND timestamp BETWEEN ? AND ?
96
96
  `).get(connectionId, processName, from, to).cnt;
97
97
  if (total <= maxPoints) {
98
- return this.db.prepare(`
99
- SELECT * FROM remote_metrics
100
- WHERE connection_id = ? AND process_name = ? AND timestamp BETWEEN ? AND ?
101
- ORDER BY timestamp ASC
98
+ return this.db.prepare(`
99
+ SELECT * FROM remote_metrics
100
+ WHERE connection_id = ? AND process_name = ? AND timestamp BETWEEN ? AND ?
101
+ ORDER BY timestamp ASC
102
102
  `).all(connectionId, processName, from, to);
103
103
  }
104
104
  // Downsample: pick every Nth row so we stay under maxPoints
105
105
  const step = Math.ceil(total / maxPoints);
106
- return this.db.prepare(`
107
- SELECT * FROM remote_metrics
108
- WHERE connection_id = ? AND process_name = ? AND timestamp BETWEEN ? AND ?
109
- AND (id % ?) = 0
110
- ORDER BY timestamp ASC
111
- LIMIT ?
106
+ return this.db.prepare(`
107
+ SELECT * FROM remote_metrics
108
+ WHERE connection_id = ? AND process_name = ? AND timestamp BETWEEN ? AND ?
109
+ AND (id % ?) = 0
110
+ ORDER BY timestamp ASC
111
+ LIMIT ?
112
112
  `).all(connectionId, processName, from, to, step, maxPoints);
113
113
  }
114
114
  // @group RemoteMetricsDB : Return latest snapshot for all processes on a connection
115
115
  getLatestSnapshot(connectionId) {
116
- return this.db.prepare(`
117
- SELECT r.*
118
- FROM remote_metrics r
119
- INNER JOIN (
120
- SELECT process_name, MAX(timestamp) AS max_ts
121
- FROM remote_metrics
122
- WHERE connection_id = ?
123
- GROUP BY process_name
124
- ) latest ON r.process_name = latest.process_name AND r.timestamp = latest.max_ts
125
- WHERE r.connection_id = ?
126
- ORDER BY r.process_name
116
+ return this.db.prepare(`
117
+ SELECT r.*
118
+ FROM remote_metrics r
119
+ INNER JOIN (
120
+ SELECT process_name, MAX(timestamp) AS max_ts
121
+ FROM remote_metrics
122
+ WHERE connection_id = ?
123
+ GROUP BY process_name
124
+ ) latest ON r.process_name = latest.process_name AND r.timestamp = latest.max_ts
125
+ WHERE r.connection_id = ?
126
+ ORDER BY r.process_name
127
127
  `).all(connectionId, connectionId);
128
128
  }
129
129
  close() {
package/package.json CHANGED
@@ -1,86 +1,86 @@
1
- {
2
- "name": "ezpm2gui",
3
- "version": "1.8.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
- "mobile": "node scripts/deploy-mobile.js",
16
- "mobile:dev": "node scripts/deploy-mobile.js dev",
17
- "mobile:android": "node scripts/deploy-mobile.js android --debug --install",
18
- "mobile:android:release": "node scripts/deploy-mobile.js android --release",
19
- "mobile:debug": "powershell -ExecutionPolicy Bypass -File scripts/Debug-Mobile.ps1",
20
- "mobile:debug:dump": "powershell -ExecutionPolicy Bypass -File scripts/Debug-Mobile.ps1 -Dump",
21
- "mobile:ps": "powershell -ExecutionPolicy Bypass -File scripts/Deploy-Mobile.ps1",
22
- "mobile:ps:release": "powershell -ExecutionPolicy Bypass -File scripts/Deploy-Mobile.ps1 -Mode release -Install -Launch",
23
- "mobile:ps:debug": "powershell -ExecutionPolicy Bypass -File scripts/Deploy-Mobile.ps1 -Mode debug -Install -Launch",
24
- "mobile:ios": "node scripts/deploy-mobile.js ios",
25
- "mobile:ios:release": "node scripts/deploy-mobile.js ios --release",
26
- "build:server": "tsc",
27
- "build:client": "cd src/client && npm run build",
28
- "build:bin": "tsc --project tsconfig.bin.json",
29
- "prepare": "npm run build",
30
- "test": "echo \"Error: no test specified\" && exit 1",
31
- "postinstall": "node scripts/postinstall.js"
32
- },
33
- "keywords": [
34
- "pm2",
35
- "gui",
36
- "monitor",
37
- "process-manager",
38
- "dashboard"
39
- ],
40
- "author": "Chandan Bhagat",
41
- "license": "AGPL-3.0-or-later",
42
- "repository": {
43
- "type": "git",
44
- "url": "git+https://github.com/thechandanbhagat/ezpm2gui.git"
45
- },
46
- "description": "A modern web-based GUI for PM2 process manager",
47
- "files": [
48
- "dist/",
49
- "bin/",
50
- "src/client/build/",
51
- "scripts/postinstall.js"
52
- ],
53
- "dependencies": {
54
- "@tailwindcss/postcss": "^4.1.14",
55
- "@types/better-sqlite3": "^7.6.13",
56
- "@types/node-cron": "^3.0.11",
57
- "axios": "^1.9.0",
58
- "better-sqlite3": "^12.9.0",
59
- "chart.js": "^4.4.9",
60
- "cron-parser": "^5.4.0",
61
- "dotenv": "^16.5.0",
62
- "express": "^4.18.3",
63
- "node-cron": "^4.2.1",
64
- "pm2": "^6.0.5",
65
- "react": "^19.1.0",
66
- "react-dom": "^19.1.0",
67
- "react-scripts": "^5.0.1",
68
- "socket.io": "^4.8.1",
69
- "ssh2": "^1.16.0",
70
- "uuid": "^11.0.5"
71
- },
72
- "devDependencies": {
73
- "@types/express": "^4.17.21",
74
- "@types/node": "^22.15.17",
75
- "@types/react": "^19.1.4",
76
- "@types/react-dom": "^19.1.5",
77
- "@types/socket.io": "^3.0.2",
78
- "@types/ssh2": "^1.15.5",
79
- "@types/uuid": "^10.0.0",
80
- "concurrently": "^9.1.2",
81
- "cross-env": "^10.1.0",
82
- "nodemon": "^3.1.10",
83
- "ts-node": "^10.9.2",
84
- "typescript": "^5.8.3"
85
- }
86
- }
1
+ {
2
+ "name": "ezpm2gui",
3
+ "version": "1.9.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
+ "mobile": "node scripts/deploy-mobile.js",
16
+ "mobile:dev": "node scripts/deploy-mobile.js dev",
17
+ "mobile:android": "node scripts/deploy-mobile.js android --debug --install",
18
+ "mobile:android:release": "node scripts/deploy-mobile.js android --release",
19
+ "mobile:debug": "powershell -ExecutionPolicy Bypass -File scripts/Debug-Mobile.ps1",
20
+ "mobile:debug:dump": "powershell -ExecutionPolicy Bypass -File scripts/Debug-Mobile.ps1 -Dump",
21
+ "mobile:ps": "powershell -ExecutionPolicy Bypass -File scripts/Deploy-Mobile.ps1",
22
+ "mobile:ps:release": "powershell -ExecutionPolicy Bypass -File scripts/Deploy-Mobile.ps1 -Mode release -Install -Launch",
23
+ "mobile:ps:debug": "powershell -ExecutionPolicy Bypass -File scripts/Deploy-Mobile.ps1 -Mode debug -Install -Launch",
24
+ "mobile:ios": "node scripts/deploy-mobile.js ios",
25
+ "mobile:ios:release": "node scripts/deploy-mobile.js ios --release",
26
+ "build:server": "tsc",
27
+ "build:client": "cd src/client && npm run build",
28
+ "build:bin": "tsc --project tsconfig.bin.json",
29
+ "prepare": "npm run build",
30
+ "test": "echo \"Error: no test specified\" && exit 1",
31
+ "postinstall": "node scripts/postinstall.js"
32
+ },
33
+ "keywords": [
34
+ "pm2",
35
+ "gui",
36
+ "monitor",
37
+ "process-manager",
38
+ "dashboard"
39
+ ],
40
+ "author": "Chandan Bhagat",
41
+ "license": "AGPL-3.0-or-later",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/thechandanbhagat/ezpm2gui.git"
45
+ },
46
+ "description": "A modern web-based GUI for PM2 process manager",
47
+ "files": [
48
+ "dist/",
49
+ "bin/",
50
+ "src/client/build/",
51
+ "scripts/postinstall.js"
52
+ ],
53
+ "dependencies": {
54
+ "@tailwindcss/postcss": "^4.1.14",
55
+ "@types/better-sqlite3": "^7.6.13",
56
+ "@types/node-cron": "^3.0.11",
57
+ "axios": "^1.9.0",
58
+ "better-sqlite3": "^12.9.0",
59
+ "chart.js": "^4.4.9",
60
+ "cron-parser": "^5.4.0",
61
+ "dotenv": "^16.5.0",
62
+ "express": "^4.18.3",
63
+ "node-cron": "^4.2.1",
64
+ "pm2": "^6.0.5",
65
+ "react": "^19.1.0",
66
+ "react-dom": "^19.1.0",
67
+ "react-scripts": "^5.0.1",
68
+ "socket.io": "^4.8.1",
69
+ "ssh2": "^1.16.0",
70
+ "uuid": "^11.0.5"
71
+ },
72
+ "devDependencies": {
73
+ "@types/express": "^4.17.21",
74
+ "@types/node": "^22.15.17",
75
+ "@types/react": "^19.1.4",
76
+ "@types/react-dom": "^19.1.5",
77
+ "@types/socket.io": "^3.0.2",
78
+ "@types/ssh2": "^1.15.5",
79
+ "@types/uuid": "^10.0.0",
80
+ "concurrently": "^9.1.2",
81
+ "cross-env": "^10.1.0",
82
+ "nodemon": "^3.1.10",
83
+ "ts-node": "^10.9.2",
84
+ "typescript": "^5.8.3"
85
+ }
86
+ }
@@ -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.9decb204.css",
4
- "main.js": "/static/js/main.28a4a583.js",
3
+ "main.css": "/static/css/main.2836d066.css",
4
+ "main.js": "/static/js/main.d5c19622.js",
5
5
  "index.html": "/index.html",
6
- "main.9decb204.css.map": "/static/css/main.9decb204.css.map",
7
- "main.28a4a583.js.map": "/static/js/main.28a4a583.js.map"
6
+ "main.2836d066.css.map": "/static/css/main.2836d066.css.map",
7
+ "main.d5c19622.js.map": "/static/js/main.d5c19622.js.map"
8
8
  },
9
9
  "entrypoints": [
10
- "static/css/main.9decb204.css",
11
- "static/js/main.28a4a583.js"
10
+ "static/css/main.2836d066.css",
11
+ "static/js/main.d5c19622.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 -->
@@ -1 +1 @@
1
- <!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#4a90e2"/><meta name="description" content="EZ PM2 GUI - A modern interface for PM2 process manager"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>EZ PM2 GUI - PM2 Process Manager</title><script defer="defer" src="/static/js/main.28a4a583.js"></script><link href="/static/css/main.9decb204.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
1
+ <!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#4a90e2"/><meta name="description" content="EZ PM2 GUI - A modern interface for PM2 process manager"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>EZ PM2 GUI - PM2 Process Manager</title><script defer="defer" src="/static/js/main.d5c19622.js"></script><link href="/static/css/main.2836d066.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>