ezpm2gui 1.2.2 → 1.3.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.
@@ -0,0 +1,407 @@
1
+ "use strict";
2
+ /**
3
+ * Cron Job Service - Manages PM2 processes with cron_restart
4
+ */
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.CronJobService = void 0;
40
+ const fs = __importStar(require("fs"));
41
+ const path = __importStar(require("path"));
42
+ const uuid_1 = require("uuid");
43
+ const pm2_connection_1 = require("../utils/pm2-connection");
44
+ const cron_parser_1 = require("cron-parser");
45
+ const CRON_CONFIG_FILE = path.join(__dirname, '../config/cron-jobs.json');
46
+ const CRON_SCRIPTS_DIR = path.join(__dirname, '../config/cron-scripts');
47
+ class CronJobService {
48
+ constructor() {
49
+ this.ensureConfigFile();
50
+ }
51
+ static getInstance() {
52
+ if (!CronJobService.instance) {
53
+ CronJobService.instance = new CronJobService();
54
+ }
55
+ return CronJobService.instance;
56
+ }
57
+ ensureConfigFile() {
58
+ const dir = path.dirname(CRON_CONFIG_FILE);
59
+ if (!fs.existsSync(dir)) {
60
+ fs.mkdirSync(dir, { recursive: true });
61
+ }
62
+ if (!fs.existsSync(CRON_CONFIG_FILE)) {
63
+ fs.writeFileSync(CRON_CONFIG_FILE, JSON.stringify([], null, 2));
64
+ }
65
+ // Ensure scripts directory exists for inline scripts
66
+ if (!fs.existsSync(CRON_SCRIPTS_DIR)) {
67
+ fs.mkdirSync(CRON_SCRIPTS_DIR, { recursive: true });
68
+ }
69
+ }
70
+ readConfig() {
71
+ try {
72
+ const data = fs.readFileSync(CRON_CONFIG_FILE, 'utf-8');
73
+ return JSON.parse(data);
74
+ }
75
+ catch (error) {
76
+ console.error('Error reading cron config:', error);
77
+ return [];
78
+ }
79
+ }
80
+ writeConfig(configs) {
81
+ fs.writeFileSync(CRON_CONFIG_FILE, JSON.stringify(configs, null, 2));
82
+ }
83
+ /**
84
+ * Validate cron expression
85
+ */
86
+ validateCronExpression(expression) {
87
+ try {
88
+ const interval = cron_parser_1.CronExpressionParser.parse(expression);
89
+ return {
90
+ valid: true,
91
+ nextRun: interval.next().toDate()
92
+ };
93
+ }
94
+ catch (error) {
95
+ return {
96
+ valid: false,
97
+ error: error.message
98
+ };
99
+ }
100
+ }
101
+ /**
102
+ * Get human-readable description of cron expression
103
+ */
104
+ getCronDescription(expression) {
105
+ try {
106
+ const parts = expression.split(' ');
107
+ if (parts.length !== 5) {
108
+ return 'Invalid cron expression';
109
+ }
110
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
111
+ // Simple descriptions for common patterns
112
+ if (expression === '* * * * *')
113
+ return 'Every minute';
114
+ if (expression === '0 * * * *')
115
+ return 'Every hour';
116
+ if (expression === '0 0 * * *')
117
+ return 'Daily at midnight';
118
+ if (expression === '0 0 * * 0')
119
+ return 'Weekly on Sunday at midnight';
120
+ if (expression === '0 0 1 * *')
121
+ return 'Monthly on the 1st at midnight';
122
+ let desc = 'At ';
123
+ if (minute === '*')
124
+ desc += 'every minute';
125
+ else
126
+ desc += `minute ${minute}`;
127
+ if (hour !== '*')
128
+ desc += ` past hour ${hour}`;
129
+ if (dayOfMonth !== '*')
130
+ desc += ` on day ${dayOfMonth}`;
131
+ if (month !== '*')
132
+ desc += ` in month ${month}`;
133
+ if (dayOfWeek !== '*')
134
+ desc += ` on day ${dayOfWeek} of week`;
135
+ return desc;
136
+ }
137
+ catch (error) {
138
+ return 'Invalid expression';
139
+ }
140
+ }
141
+ /**
142
+ * Get script path for inline scripts (creates temp file)
143
+ */
144
+ getScriptPath(config) {
145
+ if (config.scriptMode === 'file') {
146
+ return config.scriptPath;
147
+ }
148
+ // For inline scripts, write to temp file
149
+ const extension = this.getScriptExtension(config.scriptType);
150
+ const scriptFileName = `${config.id}${extension}`;
151
+ const scriptPath = path.join(CRON_SCRIPTS_DIR, scriptFileName);
152
+ if (config.inlineScript) {
153
+ fs.writeFileSync(scriptPath, config.inlineScript, 'utf8');
154
+ // Make executable for shell scripts
155
+ if (config.scriptType === 'shell') {
156
+ try {
157
+ fs.chmodSync(scriptPath, '755');
158
+ }
159
+ catch (err) {
160
+ console.warn('Could not set execute permission:', err);
161
+ }
162
+ }
163
+ }
164
+ return scriptPath;
165
+ }
166
+ /**
167
+ * Get file extension for script type
168
+ */
169
+ getScriptExtension(scriptType) {
170
+ switch (scriptType) {
171
+ case 'node':
172
+ return '.js';
173
+ case 'python':
174
+ return '.py';
175
+ case 'shell':
176
+ return '.sh';
177
+ case 'dotnet':
178
+ return '.cs';
179
+ default:
180
+ return '.txt';
181
+ }
182
+ }
183
+ /**
184
+ * Convert CronJobConfig to PM2 start options
185
+ */
186
+ toPM2Options(config) {
187
+ const scriptPath = this.getScriptPath(config);
188
+ const options = {
189
+ name: `cron-${config.id}`,
190
+ script: scriptPath,
191
+ cron_restart: config.cronExpression,
192
+ autorestart: false, // Don't auto-restart on crash, only on cron schedule
193
+ watch: false,
194
+ log_date_format: 'YYYY-MM-DD HH:mm:ss',
195
+ combine_logs: true
196
+ };
197
+ // Set interpreter based on script type
198
+ switch (config.scriptType) {
199
+ case 'node':
200
+ options.interpreter = 'node';
201
+ break;
202
+ case 'python':
203
+ options.interpreter = 'python';
204
+ break;
205
+ case 'dotnet':
206
+ options.interpreter = 'none';
207
+ options.script = 'dotnet';
208
+ options.args = ['run', '--project', config.scriptPath];
209
+ break;
210
+ case 'shell':
211
+ options.interpreter = 'bash';
212
+ options.interpreter_args = '-c';
213
+ break;
214
+ }
215
+ if (config.args && config.args.length > 0) {
216
+ options.args = config.args;
217
+ }
218
+ if (config.env) {
219
+ options.env = config.env;
220
+ }
221
+ if (config.cwd) {
222
+ options.cwd = config.cwd;
223
+ }
224
+ return options;
225
+ }
226
+ /**
227
+ * Create a new cron job
228
+ */
229
+ async createCronJob(config) {
230
+ const validation = this.validateCronExpression(config.cronExpression);
231
+ if (!validation.valid) {
232
+ throw new Error(`Invalid cron expression: ${validation.error}`);
233
+ }
234
+ const newConfig = {
235
+ ...config,
236
+ id: (0, uuid_1.v4)(),
237
+ createdAt: new Date().toISOString(),
238
+ updatedAt: new Date().toISOString()
239
+ };
240
+ const configs = this.readConfig();
241
+ configs.push(newConfig);
242
+ this.writeConfig(configs);
243
+ // Start the PM2 process if enabled
244
+ if (newConfig.enabled) {
245
+ await this.startCronJob(newConfig.id);
246
+ }
247
+ return newConfig;
248
+ }
249
+ /**
250
+ * Get all cron jobs
251
+ */
252
+ getCronJobs() {
253
+ return this.readConfig();
254
+ }
255
+ /**
256
+ * Get a single cron job by ID
257
+ */
258
+ getCronJob(id) {
259
+ const configs = this.readConfig();
260
+ return configs.find(c => c.id === id);
261
+ }
262
+ /**
263
+ * Update a cron job
264
+ */
265
+ async updateCronJob(id, updates) {
266
+ if (updates.cronExpression) {
267
+ const validation = this.validateCronExpression(updates.cronExpression);
268
+ if (!validation.valid) {
269
+ throw new Error(`Invalid cron expression: ${validation.error}`);
270
+ }
271
+ }
272
+ const configs = this.readConfig();
273
+ const index = configs.findIndex(c => c.id === id);
274
+ if (index === -1) {
275
+ throw new Error('Cron job not found');
276
+ }
277
+ const wasEnabled = configs[index].enabled;
278
+ configs[index] = {
279
+ ...configs[index],
280
+ ...updates,
281
+ id, // Ensure ID doesn't change
282
+ updatedAt: new Date().toISOString()
283
+ };
284
+ this.writeConfig(configs);
285
+ // Handle PM2 process state changes
286
+ if (wasEnabled && !configs[index].enabled) {
287
+ await this.stopCronJob(id);
288
+ }
289
+ else if (!wasEnabled && configs[index].enabled) {
290
+ await this.startCronJob(id);
291
+ }
292
+ else if (configs[index].enabled) {
293
+ // Restart if enabled and config changed
294
+ await this.stopCronJob(id);
295
+ await this.startCronJob(id);
296
+ }
297
+ return configs[index];
298
+ }
299
+ /**
300
+ * Delete a cron job
301
+ */
302
+ async deleteCronJob(id) {
303
+ const configs = this.readConfig();
304
+ const job = configs.find(c => c.id === id);
305
+ if (!job) {
306
+ throw new Error('Cron job not found');
307
+ }
308
+ // Stop PM2 process if running
309
+ if (job.enabled) {
310
+ await this.stopCronJob(id);
311
+ }
312
+ // Delete inline script file if it exists
313
+ if (job.scriptMode === 'inline') {
314
+ const extension = this.getScriptExtension(job.scriptType);
315
+ const scriptPath = path.join(CRON_SCRIPTS_DIR, `${id}${extension}`);
316
+ if (fs.existsSync(scriptPath)) {
317
+ fs.unlinkSync(scriptPath);
318
+ }
319
+ }
320
+ const filtered = configs.filter(c => c.id !== id);
321
+ this.writeConfig(filtered);
322
+ }
323
+ /**
324
+ * Start a cron job (start PM2 process with cron_restart)
325
+ */
326
+ async startCronJob(id) {
327
+ const config = this.getCronJob(id);
328
+ if (!config) {
329
+ throw new Error('Cron job not found');
330
+ }
331
+ const pm2Options = this.toPM2Options(config);
332
+ const pm2 = require('pm2');
333
+ await (0, pm2_connection_1.executePM2Command)((callback) => {
334
+ pm2.start(pm2Options, callback);
335
+ });
336
+ }
337
+ /**
338
+ * Stop a cron job (delete PM2 process)
339
+ */
340
+ async stopCronJob(id) {
341
+ const processName = `cron-${id}`;
342
+ const pm2 = require('pm2');
343
+ try {
344
+ await (0, pm2_connection_1.executePM2Command)((callback) => {
345
+ pm2.delete(processName, (err) => {
346
+ // PM2 delete returns undefined on success, pass empty object
347
+ if (err && !err.message.includes('not found')) {
348
+ callback(err);
349
+ }
350
+ else {
351
+ callback(null, {});
352
+ }
353
+ });
354
+ });
355
+ }
356
+ catch (error) {
357
+ // Ignore not found errors
358
+ if (!error.message.includes('not found')) {
359
+ throw error;
360
+ }
361
+ }
362
+ }
363
+ /**
364
+ * Get status of all cron jobs (including PM2 process info)
365
+ */
366
+ async getCronJobsStatus() {
367
+ const configs = this.readConfig();
368
+ const pm2 = require('pm2');
369
+ const list = await (0, pm2_connection_1.executePM2Command)((callback) => {
370
+ pm2.list(callback);
371
+ });
372
+ const statuses = configs.map(config => {
373
+ var _a;
374
+ const processName = `cron-${config.id}`;
375
+ const pm2Process = list.find((p) => p.name === processName);
376
+ let nextExecution;
377
+ if (config.enabled) {
378
+ try {
379
+ const interval = cron_parser_1.CronExpressionParser.parse(config.cronExpression);
380
+ nextExecution = interval.next().toDate().toISOString();
381
+ }
382
+ catch (e) {
383
+ // Ignore
384
+ }
385
+ }
386
+ return {
387
+ config,
388
+ pm2Process,
389
+ isRunning: ((_a = pm2Process === null || pm2Process === void 0 ? void 0 : pm2Process.pm2_env) === null || _a === void 0 ? void 0 : _a.status) === 'online',
390
+ nextExecution
391
+ };
392
+ });
393
+ return statuses;
394
+ }
395
+ /**
396
+ * Toggle cron job enabled state
397
+ */
398
+ async toggleCronJob(id) {
399
+ const config = this.getCronJob(id);
400
+ if (!config) {
401
+ throw new Error('Cron job not found');
402
+ }
403
+ return this.updateCronJob(id, { enabled: !config.enabled });
404
+ }
405
+ }
406
+ exports.CronJobService = CronJobService;
407
+ exports.default = CronJobService.getInstance();
@@ -56,8 +56,14 @@ export declare class RemoteConnection extends EventEmitter {
56
56
  executeCommand(command: string, forceSudo?: boolean): Promise<CommandResult>;
57
57
  /**
58
58
  * Check if PM2 is installed on the remote server
59
+ * Uses multiple detection methods for better reliability
59
60
  */
60
61
  checkPM2Installation(): Promise<boolean>;
62
+ /**
63
+ * Execute a PM2 command with proper PATH handling
64
+ * Tries different methods to find and execute PM2
65
+ */
66
+ private executePM2Command;
61
67
  /**
62
68
  * Get PM2 processes from the remote server
63
69
  */
@@ -85,6 +91,10 @@ export declare class RemoteConnection extends EventEmitter {
85
91
  * Delete a PM2 process
86
92
  */
87
93
  deletePM2Process(processName: string): Promise<CommandResult>;
94
+ /**
95
+ * Install PM2 on the remote server
96
+ */
97
+ installPM2(): Promise<CommandResult>;
88
98
  /**
89
99
  * Get system information from the remote server
90
100
  */
@@ -116,6 +126,13 @@ export declare class RemoteConnectionManager {
116
126
  * @returns The connection ID
117
127
  */
118
128
  createConnection(config: RemoteConnectionConfig): string;
129
+ /**
130
+ * Update an existing remote connection
131
+ * @param connectionId The connection ID
132
+ * @param config New connection configuration
133
+ * @returns True if the connection was updated, false if it didn't exist
134
+ */
135
+ updateConnection(connectionId: string, config: RemoteConnectionConfig): Promise<boolean>;
119
136
  /**
120
137
  * Get a connection by ID
121
138
  * @param connectionId The connection ID
@@ -172,18 +172,108 @@ class RemoteConnection extends events_1.EventEmitter {
172
172
  }
173
173
  /**
174
174
  * Check if PM2 is installed on the remote server
175
+ * Uses multiple detection methods for better reliability
175
176
  */
176
177
  async checkPM2Installation() {
177
178
  try {
178
- const result = await this.executeCommand('which pm2 || echo "not installed"');
179
- this.isPM2Installed = !result.stdout.includes('not installed');
180
- return this.isPM2Installed;
179
+ // Method 1: Try running pm2 --version directly (most reliable)
180
+ try {
181
+ const versionResult = await this.executeCommand('pm2 --version');
182
+ if (versionResult.code === 0 && versionResult.stdout.trim()) {
183
+ console.log(`PM2 detected via version check: ${versionResult.stdout.trim()}`);
184
+ this.isPM2Installed = true;
185
+ return true;
186
+ }
187
+ }
188
+ catch (error) {
189
+ console.log('PM2 version check failed, trying alternative methods');
190
+ }
191
+ // Method 2: Check common installation paths
192
+ const pathChecks = [
193
+ 'which pm2',
194
+ 'command -v pm2',
195
+ 'ls -la /usr/local/bin/pm2',
196
+ 'ls -la ~/.npm-global/bin/pm2',
197
+ 'ls -la ~/node_modules/.bin/pm2',
198
+ 'find /usr -name "pm2" -type f -executable 2>/dev/null | head -1'
199
+ ];
200
+ for (const pathCheck of pathChecks) {
201
+ try {
202
+ const result = await this.executeCommand(pathCheck);
203
+ if (result.code === 0 && result.stdout.trim() && !result.stdout.includes('not found')) {
204
+ console.log(`PM2 found via: ${pathCheck} -> ${result.stdout.trim()}`);
205
+ this.isPM2Installed = true;
206
+ return true;
207
+ }
208
+ }
209
+ catch (error) {
210
+ // Continue to next method
211
+ continue;
212
+ }
213
+ }
214
+ // Method 3: Try with full environment loading
215
+ try {
216
+ const envResult = await this.executeCommand('bash -l -c "pm2 --version"');
217
+ if (envResult.code === 0 && envResult.stdout.trim()) {
218
+ console.log(`PM2 detected via bash login shell: ${envResult.stdout.trim()}`);
219
+ this.isPM2Installed = true;
220
+ return true;
221
+ }
222
+ }
223
+ catch (error) {
224
+ console.log('PM2 not found via bash login shell');
225
+ }
226
+ // Method 4: Try npm ls -g pm2
227
+ try {
228
+ const npmResult = await this.executeCommand('npm ls -g pm2 --depth=0');
229
+ if (npmResult.code === 0 && !npmResult.stdout.includes('(empty)')) {
230
+ console.log('PM2 detected via npm global list');
231
+ this.isPM2Installed = true;
232
+ return true;
233
+ }
234
+ }
235
+ catch (error) {
236
+ console.log('PM2 not found via npm global list');
237
+ }
238
+ console.log('PM2 not detected via any method');
239
+ this.isPM2Installed = false;
240
+ return false;
181
241
  }
182
242
  catch (error) {
243
+ console.error('Error during PM2 installation check:', error);
183
244
  this.isPM2Installed = false;
184
245
  return false;
185
246
  }
186
247
  }
248
+ /**
249
+ * Execute a PM2 command with proper PATH handling
250
+ * Tries different methods to find and execute PM2
251
+ */
252
+ 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
+ ];
261
+ let lastError = null;
262
+ for (const command of commands) {
263
+ try {
264
+ const result = await this.executeCommand(command);
265
+ if (result.code === 0) {
266
+ return result;
267
+ }
268
+ lastError = new Error(`Command failed with exit code ${result.code}: ${result.stderr}`);
269
+ }
270
+ catch (error) {
271
+ lastError = error;
272
+ continue;
273
+ }
274
+ }
275
+ throw lastError || new Error(`Failed to execute PM2 command: ${pm2Args}`);
276
+ }
187
277
  /**
188
278
  * Get PM2 processes from the remote server
189
279
  */
@@ -193,14 +283,14 @@ class RemoteConnection extends events_1.EventEmitter {
193
283
  if (!this._isConnected) {
194
284
  await this.connect();
195
285
  }
196
- // Check if PM2 is installed
197
- if (!this.isPM2Installed) {
198
- const isPM2Installed = await this.checkPM2Installation();
199
- if (!isPM2Installed) {
200
- throw new Error('PM2 is not installed on the remote server');
201
- }
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');
202
290
  }
203
- const result = await this.executeCommand('pm2 jlist');
291
+ // Update the cached status
292
+ this.isPM2Installed = true;
293
+ const result = await this.executePM2Command('jlist');
204
294
  // Clean the output to ensure it's valid JSON
205
295
  // Sometimes pm2 jlist can include non-JSON data at the beginning or end
206
296
  let cleanedOutput = result.stdout.trim();
@@ -289,25 +379,72 @@ class RemoteConnection extends events_1.EventEmitter {
289
379
  * Start a PM2 process
290
380
  */
291
381
  async startPM2Process(processName) {
292
- return await this.executeCommand(`pm2 start ${processName}`);
382
+ return await this.executePM2Command(`start ${processName}`);
293
383
  }
294
384
  /**
295
385
  * Stop a PM2 process
296
386
  */
297
387
  async stopPM2Process(processName) {
298
- return await this.executeCommand(`pm2 stop ${processName}`);
388
+ return await this.executePM2Command(`stop ${processName}`);
299
389
  }
300
390
  /**
301
391
  * Restart a PM2 process
302
392
  */
303
393
  async restartPM2Process(processName) {
304
- return await this.executeCommand(`pm2 restart ${processName}`);
394
+ return await this.executePM2Command(`restart ${processName}`);
305
395
  }
306
396
  /**
307
397
  * Delete a PM2 process
308
398
  */
309
399
  async deletePM2Process(processName) {
310
- return await this.executeCommand(`pm2 delete ${processName}`);
400
+ return await this.executePM2Command(`delete ${processName}`);
401
+ }
402
+ /**
403
+ * Install PM2 on the remote server
404
+ */
405
+ async installPM2() {
406
+ try {
407
+ console.log('Attempting to install PM2 on remote server...');
408
+ // Try different installation methods
409
+ const installCommands = [
410
+ 'npm install -g pm2', // Standard global install
411
+ 'sudo npm install -g pm2', // With sudo if needed
412
+ 'npm install -g pm2 --unsafe-perm=true', // With unsafe permissions
413
+ ];
414
+ let lastResult = null;
415
+ for (const command of installCommands) {
416
+ try {
417
+ const result = await this.executeCommand(command, command.includes('sudo'));
418
+ if (result.code === 0) {
419
+ console.log(`PM2 installed successfully with: ${command}`);
420
+ // Verify installation
421
+ const isPM2Installed = await this.checkPM2Installation();
422
+ if (isPM2Installed) {
423
+ this.isPM2Installed = true;
424
+ return result;
425
+ }
426
+ }
427
+ lastResult = result;
428
+ }
429
+ catch (error) {
430
+ console.log(`Failed to install PM2 with command: ${command}`);
431
+ lastResult = {
432
+ stdout: '',
433
+ stderr: error instanceof Error ? error.message : 'Unknown error',
434
+ code: 1
435
+ };
436
+ }
437
+ }
438
+ return lastResult || {
439
+ stdout: '',
440
+ stderr: 'Failed to install PM2 with any method',
441
+ code: 1
442
+ };
443
+ }
444
+ catch (error) {
445
+ console.error('Error installing PM2:', error);
446
+ throw error;
447
+ }
311
448
  }
312
449
  /**
313
450
  * Get system information from the remote server
@@ -455,6 +592,38 @@ class RemoteConnectionManager {
455
592
  this.saveConnectionsToDisk();
456
593
  return connectionId;
457
594
  }
595
+ /**
596
+ * Update an existing remote connection
597
+ * @param connectionId The connection ID
598
+ * @param config New connection configuration
599
+ * @returns True if the connection was updated, false if it didn't exist
600
+ */
601
+ async updateConnection(connectionId, config) {
602
+ const existingConnection = this.connections.get(connectionId);
603
+ if (!existingConnection) {
604
+ return false;
605
+ }
606
+ // Close the existing connection if it's active
607
+ if (existingConnection.isConnected()) {
608
+ await existingConnection.disconnect();
609
+ }
610
+ // Get the old config to preserve password if not provided in update
611
+ const oldConfig = existingConnection.getFullConfig();
612
+ // If password is not provided in the update, keep the old one
613
+ const updatedConfig = {
614
+ ...config,
615
+ password: config.password || oldConfig.password,
616
+ privateKey: config.privateKey || oldConfig.privateKey,
617
+ passphrase: config.passphrase || oldConfig.passphrase
618
+ };
619
+ // Create a new connection with updated config
620
+ const newConnection = new RemoteConnection(updatedConfig);
621
+ // Replace the old connection with the new one
622
+ this.connections.set(connectionId, newConnection);
623
+ // Save the updated connections to disk
624
+ this.saveConnectionsToDisk();
625
+ return true;
626
+ }
458
627
  /**
459
628
  * Get a connection by ID
460
629
  * @param connectionId The connection ID