@drocketxx/pm2me 1.1.37 → 1.1.38

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.
@@ -8,7 +8,7 @@ import path from 'path';
8
8
  import { fileURLToPath } from 'url';
9
9
  import fs from 'fs';
10
10
  import crypto from 'crypto';
11
- import { exec } from 'child_process';
11
+ import { exec, execSync, execFile } from 'child_process';
12
12
  import util from 'util';
13
13
  import bcrypt from 'bcrypt';
14
14
  import os from 'os';
@@ -16,9 +16,6 @@ import os from 'os';
16
16
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
17
17
  const getWorkspacesDir = () => db.data.settings?.appsPath || path.resolve(__dirname, '../../apps');
18
18
 
19
- // Track active build processes to prevent concurrent builds
20
- const activeBuildProcesses = new Map();
21
-
22
19
  const router = express.Router();
23
20
 
24
21
  // GET all PM2 apps
@@ -44,6 +41,7 @@ router.get('/system/stats', async (req, res) => {
44
41
 
45
42
  // GET PM2 version and update check
46
43
  const execAsync = util.promisify(exec);
44
+ const execFileAsync = util.promisify(execFile);
47
45
  router.get('/pm2/version-check', async (req, res) => {
48
46
  try {
49
47
  // Check globally installed PM2 version via npm (avoids local node_modules/.bin/pm2)
@@ -107,11 +105,21 @@ router.post('/pm2/:action', async (req, res) => {
107
105
  else if (action === 'reload') result = await pm2Service.reloadApp(nameOrId);
108
106
  else if (action === 'delete') result = await pm2Service.deleteApp(nameOrId);
109
107
 
110
- // Find app by name to emit system log
108
+ // Find app by name to emit system log and save to history
111
109
  const app = db.data.apps.find(a => a.name === nameOrId);
112
- if (app && req.io) {
110
+ if (app) {
113
111
  const actionLabel = action.charAt(0).toUpperCase() + action.slice(1);
114
- req.io.emit(`deploy-log-${app.id}`, `[pm2me] ${actionLabel}`);
112
+ const logString = `[pm2me] Action: ${actionLabel} executed manually by user\n`;
113
+
114
+ if (req.io) {
115
+ req.io.emit(`deploy-log-${app.id}`, logString.trim());
116
+ }
117
+
118
+ // Append to deploy log
119
+ const logFilePath = path.join(getWorkspacesDir(), `${app.id}_deploy.log`);
120
+ if (fs.existsSync(logFilePath)) {
121
+ fs.appendFileSync(logFilePath, logString);
122
+ }
115
123
  }
116
124
 
117
125
  res.json({ success: true, result });
@@ -153,18 +161,66 @@ router.get('/apps/:id/sync-status', async (req, res) => {
153
161
  }
154
162
  });
155
163
 
164
+ // GET native PM2 logs - reads directly from C:\Users\xxx\.pm2\logs\appname-out.log
165
+ router.get('/apps/:id/pm2-logs', async (req, res) => {
166
+ const { id } = req.params;
167
+ const appConfig = db.data.apps.find(a => a.id === id);
168
+ if (!appConfig) return res.status(404).send('App not found');
169
+
170
+ try {
171
+ await pm2Service.connectPM2();
172
+ const list = await pm2Service.listApps();
173
+ const pmApp = list.find(a => a.name === appConfig.name);
174
+
175
+ if (!pmApp?.pm2_env) {
176
+ return res.send(`App "${appConfig.name}" is not currently running in PM2.`);
177
+ }
178
+
179
+ const readTail = (filePath, maxLines) => {
180
+ if (!filePath || !fs.existsSync(filePath)) return [];
181
+ const size = fs.statSync(filePath).size;
182
+ if (size === 0) return [];
183
+ const readBytes = Math.min(size, 100000);
184
+ const buf = Buffer.alloc(readBytes);
185
+ const fd = fs.openSync(filePath, 'r');
186
+ fs.readSync(fd, buf, 0, readBytes, size - readBytes);
187
+ fs.closeSync(fd);
188
+ return buf.toString('utf8').split('\n').filter(l => l.trim()).slice(-maxLines);
189
+ };
190
+
191
+ const outLines = readTail(pmApp.pm2_env.pm_out_log_path, 200);
192
+ const errLines = readTail(pmApp.pm2_env.pm_err_log_path, 50)
193
+ .map(l => `[stderr] ${l}`);
194
+
195
+ const all = [...outLines, ...errLines];
196
+ res.send(all.join('\n'));
197
+ } catch (err) {
198
+ res.status(500).send(`Error reading PM2 logs: ${err.message}`);
199
+ }
200
+ });
201
+
202
+
156
203
  router.put('/apps/:id', async (req, res) => {
157
204
  const { id } = req.params;
158
205
  const index = db.data.apps.findIndex(app => app.id === id);
159
206
  if (index === -1) return res.status(404).json({ error: 'App not found' });
160
207
 
208
+ const oldApp = db.data.apps[index];
209
+ const newName = req.body.name;
210
+ const isNameChanged = newName && oldApp.name && newName !== oldApp.name;
211
+
161
212
  // Merge new config, but preserve id and status
162
213
  db.data.apps[index] = {
163
- ...db.data.apps[index],
214
+ ...oldApp,
164
215
  ...req.body,
165
- id: db.data.apps[index].id,
166
- status: db.data.apps[index].status
216
+ id: oldApp.id,
217
+ status: oldApp.status
167
218
  };
219
+
220
+ if (isNameChanged) {
221
+ db.data.apps[index].previousName = oldApp.name;
222
+ }
223
+
168
224
  await db.write();
169
225
  res.json(db.data.apps[index]);
170
226
  });
@@ -268,6 +324,84 @@ router.get('/git/repositories', async (req, res) => {
268
324
  }
269
325
  });
270
326
 
327
+ const buildPM2StartOptions = (appConfig, targetPath, updateEnv = false) => {
328
+ let pmArgs = [];
329
+ if (appConfig.pm2Args) pmArgs = appConfig.pm2Args.split(' ').filter(Boolean);
330
+
331
+ let scriptName = appConfig.pm2Script || 'npm';
332
+
333
+ // To prevent Windows console popups caused by `npm` spawning sub-processes,
334
+ // we intercept 'npm run [script]' and extract the direct 'node' command from package.json
335
+ if (os.platform() === 'win32' && scriptName.toLowerCase() === 'npm') {
336
+ const isRun = pmArgs[0] === 'run';
337
+ const targetNpmScript = isRun ? pmArgs[1] : (pmArgs[0] === 'start' ? 'start' : null);
338
+
339
+ if (targetNpmScript) {
340
+ try {
341
+ const pkgPath = path.join(targetPath, 'package.json');
342
+ if (fs.existsSync(pkgPath)) {
343
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
344
+ if (pkg.scripts && pkg.scripts[targetNpmScript]) {
345
+ const rawCommand = pkg.scripts[targetNpmScript].trim();
346
+ // If it's a simple node command, bypass npm entirely
347
+ if (rawCommand.startsWith('node ')) {
348
+ const parsedArgs = rawCommand.substring(5).trim().split(' ');
349
+ scriptName = parsedArgs[0]; // e.g. dist/server.js
350
+ pmArgs = parsedArgs.slice(1);
351
+ }
352
+ }
353
+ }
354
+ } catch (e) {
355
+ console.error('Failed to parse package.json for script resolution', e);
356
+ }
357
+ }
358
+ }
359
+
360
+ if (os.platform() === 'win32') {
361
+ if (scriptName === 'npm') {
362
+ try {
363
+ const root = execSync('npm root -g', { windowsHide: true }).toString().trim();
364
+ const cliPath = path.join(root, 'npm', 'bin', 'npm-cli.js');
365
+ if (fs.existsSync(cliPath)) {
366
+ scriptName = cliPath;
367
+ } else {
368
+ scriptName += '.cmd';
369
+ }
370
+ } catch (e) {
371
+ scriptName += '.cmd';
372
+ }
373
+ } else if (['yarn', 'pnpm', 'npx'].includes(scriptName)) {
374
+ scriptName += '.cmd';
375
+ }
376
+ }
377
+
378
+ let startOptions = {
379
+ name: appConfig.name,
380
+ cwd: targetPath,
381
+ script: scriptName,
382
+ args: pmArgs,
383
+ env: appConfig.env || {}
384
+ };
385
+
386
+ if (updateEnv) startOptions.updateEnv = true;
387
+
388
+ if (os.platform() === 'win32' && scriptName.endsWith('.cmd')) {
389
+ startOptions.interpreter = 'none';
390
+ startOptions.windowsHide = true;
391
+ } else if (!scriptName.endsWith('.js') && !scriptName.endsWith('.mjs')) {
392
+ startOptions.interpreter = 'none';
393
+ }
394
+
395
+ if (os.platform() === 'win32') {
396
+ startOptions.windowsHide = true;
397
+ }
398
+
399
+ if (appConfig.ecosystemFile) {
400
+ return path.join(targetPath, appConfig.ecosystemFile);
401
+ }
402
+ return startOptions;
403
+ };
404
+
271
405
  const rollback = async (appId, appConfig, io, targetPath, lastStep, logProcess, setPipelineState, logFilePath) => {
272
406
  if (!appConfig.lastSuccessfulCommitHash || appConfig.lastSuccessfulCommitHash === appConfig.commitHash) {
273
407
  logProcess('No stable version to rollback to or already on last good version.', true);
@@ -300,7 +434,7 @@ const rollback = async (appId, appConfig, io, targetPath, lastStep, logProcess,
300
434
  .filter(Boolean)
301
435
  .join(' && ');
302
436
  await new Promise((resolve, reject) => {
303
- const child = exec(normalizedScript, { cwd: targetPath, maxBuffer: 10 * 1024 * 1024 });
437
+ const child = exec(normalizedScript, { cwd: targetPath, maxBuffer: 10 * 1024 * 1024, windowsHide: true });
304
438
  child.stdout.on('data', data => { io.emit(`deploy-log-${appId}`, data.toString()); fs.appendFileSync(logFilePath, data); });
305
439
  child.stderr.on('data', data => { io.emit(`deploy-log-${appId}`, data.toString()); fs.appendFileSync(logFilePath, data); });
306
440
  child.on('close', code => code !== 0 ? reject(new Error('Rollback build failed')) : resolve());
@@ -310,13 +444,7 @@ const rollback = async (appId, appConfig, io, targetPath, lastStep, logProcess,
310
444
  // 5. Restart PM2 with original version
311
445
  logProcess('Restarting PM2 with original version...', true);
312
446
  await pm2Service.connectPM2();
313
- const startOpts = {
314
- name: appConfig.name,
315
- cwd: targetPath,
316
- script: appConfig.pm2Script || 'npm',
317
- updateEnv: true,
318
- env: appConfig.env || {}
319
- };
447
+ const startOpts = buildPM2StartOptions(appConfig, targetPath, true);
320
448
 
321
449
  try {
322
450
  await pm2Service.reloadApp(appConfig.name, startOpts);
@@ -363,7 +491,7 @@ export const performDeployment = async (appId, io) => {
363
491
  fs.mkdirSync(wsDir, { recursive: true });
364
492
  }
365
493
 
366
- fs.writeFileSync(logFilePath, `--- Deployment Started at ${new Date().toISOString()} ---\n`);
494
+ fs.appendFileSync(logFilePath, `\n\n--- Deployment Started at ${new Date().toISOString()} ---\n`);
367
495
 
368
496
  const logProcess = (msg, isSystem = false) => {
369
497
  const formattedMsg = isSystem ? `[pm2me] ${msg}` : msg;
@@ -399,24 +527,6 @@ export const performDeployment = async (appId, io) => {
399
527
  fs.writeFileSync(path.join(targetPath, '.env'), envFileData);
400
528
 
401
529
  if (appConfig.buildScript) {
402
- // Kill any existing build process for this app
403
- if (activeBuildProcesses.has(appId)) {
404
- const existingProcess = activeBuildProcesses.get(appId);
405
- logProcess('Killing existing build process...', true);
406
- try {
407
- existingProcess.kill('SIGTERM');
408
- // Give it a moment to terminate gracefully
409
- await new Promise(resolve => setTimeout(resolve, 1000));
410
- if (!existingProcess.killed) {
411
- existingProcess.kill('SIGKILL');
412
- }
413
- activeBuildProcesses.delete(appId);
414
- logProcess('Existing build process terminated', true);
415
- } catch (killErr) {
416
- logProcess(`Warning: Failed to kill existing build process: ${killErr.message}`, true);
417
- }
418
- }
419
-
420
530
  const normalizedScript = appConfig.buildScript.split('\n')
421
531
  .map(s => s.trim())
422
532
  .filter(Boolean)
@@ -427,22 +537,13 @@ export const performDeployment = async (appId, io) => {
427
537
  logProcess('Executing Build Script', true);
428
538
  await new Promise((resolve, reject) => {
429
539
  const child = exec(normalizedScript, { cwd: targetPath, maxBuffer: 10 * 1024 * 1024, windowsHide: true });
430
-
431
- // Store the build process
432
- activeBuildProcesses.set(appId, child);
433
-
434
540
  child.stdout.on('data', data => { io.emit(`deploy-log-${appId}`, data.toString()); fs.appendFileSync(logFilePath, data); });
435
541
  child.stderr.on('data', data => { io.emit(`deploy-log-${appId}`, data.toString()); fs.appendFileSync(logFilePath, data); });
436
542
  child.on('close', code => {
437
- // Remove from active processes when done
438
- activeBuildProcesses.delete(appId);
439
543
  if (code !== 0) reject(new Error(`Build script exited with code ${code}`));
440
544
  else { logProcess('Build Script Completed', true); resolve(); }
441
545
  });
442
- child.on('error', err => {
443
- activeBuildProcesses.delete(appId);
444
- reject(err);
445
- });
546
+ child.on('error', err => { reject(err); });
446
547
  });
447
548
  }
448
549
 
@@ -451,32 +552,47 @@ export const performDeployment = async (appId, io) => {
451
552
  logProcess('Starting PM2', true);
452
553
  await pm2Service.connectPM2();
453
554
 
454
- let pmArgs = ['start'];
455
- if (appConfig.pm2Args) pmArgs = appConfig.pm2Args.split(' ').filter(Boolean);
555
+ const startOptions = buildPM2StartOptions(appConfig, targetPath, false);
456
556
 
457
- let startOptions = {
458
- name: appConfig.name,
459
- cwd: targetPath,
460
- script: appConfig.pm2Script || 'npm',
461
- args: appConfig.pm2Script === 'npm' ? pmArgs : [],
462
- env: appConfig.env || {}
463
- };
557
+ logProcess('Updating Application in PM2', true);
464
558
 
465
- if (appConfig.ecosystemFile) startOptions = path.join(targetPath, appConfig.ecosystemFile);
559
+ // Delete previous process if the app was renamed
560
+ if (appConfig.previousName) {
561
+ logProcess(`App renamed. Removing old PM2 process: ${appConfig.previousName}`, true);
562
+ try {
563
+ await pm2Service.deleteApp(appConfig.previousName);
564
+ } catch (err) {
565
+ // If it wasn't running or already deleted, ignore.
566
+ }
567
+ delete appConfig.previousName;
568
+ await db.write();
569
+ }
466
570
 
467
- logProcess('Updating Application in PM2', true);
468
571
  let exists = null;
469
572
  try {
470
573
  const list = await pm2Service.listApps();
471
574
  exists = list.find(a => a.name === appConfig.name);
472
575
  if (exists) {
473
- const updateOptions = { ...startOptions, updateEnv: true };
474
- if (appConfig.zeroDowntime !== false) {
475
- logProcess('Zero-downtime Reloading with new environment', true);
476
- await pm2Service.reloadApp(appConfig.name, updateOptions);
576
+ const currentInterpreter = exists.pm2_env ? exists.pm2_env.exec_interpreter : null;
577
+ const currentScriptPath = exists.pm2_env ? exists.pm2_env.pm_exec_path : null;
578
+ const scriptBaseName = currentScriptPath ? path.basename(currentScriptPath) : null;
579
+
580
+ const interpreterChanged = startOptions.interpreter && currentInterpreter !== startOptions.interpreter;
581
+ const scriptChanged = scriptBaseName !== null && scriptBaseName !== startOptions.script;
582
+
583
+ if (interpreterChanged || scriptChanged) {
584
+ logProcess('Runtime configuration changed (Script or Interpreter). Recreating process...', true);
585
+ await pm2Service.deleteApp(appConfig.name);
586
+ await pm2Service.startApp(startOptions);
477
587
  } else {
478
- logProcess('Restarting (Non Zero-downtime) with new environment', true);
479
- await pm2Service.restartApp(appConfig.name, updateOptions);
588
+ const updateOptions = { ...startOptions, updateEnv: true };
589
+ if (appConfig.zeroDowntime !== false) {
590
+ logProcess('Zero-downtime Reloading with new environment', true);
591
+ await pm2Service.reloadApp(appConfig.name, updateOptions);
592
+ } else {
593
+ logProcess('Restarting (Non Zero-downtime) with new environment', true);
594
+ await pm2Service.restartApp(appConfig.name, updateOptions);
595
+ }
480
596
  }
481
597
  } else {
482
598
  logProcess('Fresh Starting', true);
@@ -562,6 +678,14 @@ export const performDeployment = async (appId, io) => {
562
678
  // Build and Deploy an App
563
679
  router.post('/deploy/:appId', async (req, res) => {
564
680
  try {
681
+ const app = db.data.apps.find(a => a.id === req.params.appId);
682
+ if (app) {
683
+ const logFilePath = path.join(getWorkspacesDir(), `${app.id}_deploy.log`);
684
+ const deployString = `[pm2me] Action: Manual deployment triggered by user\n`;
685
+ if (fs.existsSync(logFilePath)) fs.appendFileSync(logFilePath, deployString);
686
+ if (req.io) req.io.emit(`deploy-log-${app.id}`, deployString.trim());
687
+ }
688
+
565
689
  await performDeployment(req.params.appId, req.io);
566
690
  res.json({ success: true });
567
691
  } catch (err) {
@@ -575,50 +699,16 @@ router.get('/deploy/:appId/logs', async (req, res) => {
575
699
  const appConfig = db.data.apps.find(a => a.id === appId);
576
700
  if (!appConfig) return res.status(404).json({ error: 'App not found' });
577
701
 
578
- const logFilePath = path.join(getWorkspacesDir(), `${appConfig.name}_deploy.log`);
702
+ const logFilePath = path.join(getWorkspacesDir(), `${appConfig.id}_deploy.log`);
579
703
  let logs = '';
580
704
 
581
705
  if (fs.existsSync(logFilePath)) {
582
706
  logs += fs.readFileSync(logFilePath, 'utf8');
583
707
  }
584
708
 
585
- try {
586
- await pm2Service.connectPM2();
587
- const list = await pm2Service.listApps();
588
- const pmApp = list.find(app => app.name === appConfig.name);
589
- if (pmApp && pmApp.pm2_env) {
590
- const outPath = pmApp.pm2_env.pm_out_log_path;
591
- const errPath = pmApp.pm2_env.pm_err_log_path;
592
-
593
- const readTailLines = (filePath, prefix = '', maxLines = 100) => {
594
- if (!filePath || !fs.existsSync(filePath)) return '';
595
- const stats = fs.statSync(filePath);
596
- const size = stats.size;
597
- const maxBytes = 50000;
598
- const start = Math.max(0, size - maxBytes);
599
- const buffer = Buffer.alloc(Math.max(0, size - start));
600
- if (buffer.length === 0) return '';
601
- const fd = fs.openSync(filePath, 'r');
602
- fs.readSync(fd, buffer, 0, buffer.length, start);
603
- fs.closeSync(fd);
604
- const text = buffer.toString('utf8');
605
- const lines = text.split('\n').filter(l => l.trim());
606
- return lines.slice(-maxLines).map(line => `${prefix} | ${line}`).join('\n');
607
- };
608
-
609
- const outLogs = readTailLines(outPath, `[out]`);
610
- const errLogs = readTailLines(errPath, `[err]`);
611
-
612
- if (outLogs || errLogs) {
613
- logs += '\n\n--- 🔵 PM2 Execution Logs ---\n';
614
- if (errLogs) logs += errLogs + '\n';
615
- if (outLogs) logs += outLogs + '\n';
616
- }
617
- }
618
- } catch (err) {
619
- console.error('Failed to fetch PM2 logs', err);
620
- }
621
-
709
+ // Return only deployment events - PM2 native logs are served separately via /pm2-logs
710
+ // Return just the historically preserved unified log file
711
+ // Realtime events are handled via PM2 Event bus in app.js and broadcasted automatically
622
712
  res.send(logs);
623
713
  });
624
714
 
@@ -698,6 +788,17 @@ router.post('/webhook', async (req, res) => {
698
788
  for (const app of appsToDeploy) {
699
789
  try {
700
790
  await notificationService.notifyAll(app.name, 'Push event received, triggering auto-sync...');
791
+
792
+ // Append push event to Event Log history
793
+ const logFilePath = path.join(getWorkspacesDir(), `${app.id}_deploy.log`);
794
+ const pushLogString = `[pm2me] Auto-Sync triggered via GitHub Webhook (Branch: ${branch})\n`;
795
+ if (fs.existsSync(logFilePath)) {
796
+ fs.appendFileSync(logFilePath, pushLogString);
797
+ }
798
+ if (req.io) {
799
+ req.io.emit(`deploy-log-${app.id}`, pushLogString.trim());
800
+ }
801
+
701
802
  // Trigger deployment directly
702
803
  performDeployment(app.id, req.io).catch(err => {
703
804
  console.error(`Auto-Sync failed for ${app.name}:`, err);
@@ -753,12 +854,15 @@ router.get('/nginx/info', async (req, res) => {
753
854
 
754
855
  // Check if nginx binary exists / is installed
755
856
  try {
756
- const checkCmd = isWindows
757
- ? `"${NGINX_BIN}" -v`
758
- : 'nginx -v';
759
- const { stderr } = await execAsync(checkCmd);
760
- const match = (stderr || '').match(/nginx\/([\\d.]+)/);
761
- info.version = match ? match[1] : 'unknown';
857
+ if (isWindows) {
858
+ const { stderr } = await execFileAsync(NGINX_BIN, ['-v'], { windowsHide: true });
859
+ const match = (stderr || '').match(/nginx\/([\d.]+)/);
860
+ info.version = match ? match[1] : 'unknown';
861
+ } else {
862
+ const { stderr } = await execAsync(`${NGINX_BIN} -v`, { windowsHide: true });
863
+ const match = (stderr || '').match(/nginx\/([\d.]+)/);
864
+ info.version = match ? match[1] : 'unknown';
865
+ }
762
866
  info.installed = true;
763
867
  } catch {
764
868
  info.installed = false;
@@ -776,10 +880,10 @@ router.get('/nginx/status', async (req, res) => {
776
880
  let running = false;
777
881
  try {
778
882
  if (isWindows) {
779
- const { stdout } = await execAsync('tasklist /FI "IMAGENAME eq nginx.exe" /NH');
883
+ const { stdout } = await execFileAsync('tasklist.exe', ['/FI', 'IMAGENAME eq nginx.exe', '/NH'], { windowsHide: true });
780
884
  running = stdout.toLowerCase().includes('nginx.exe');
781
885
  } else {
782
- const { stdout } = await execAsync('pgrep -x nginx');
886
+ const { stdout } = await execAsync('pgrep -x nginx', { windowsHide: true });
783
887
  running = stdout.trim().length > 0;
784
888
  }
785
889
  } catch {
@@ -867,7 +971,7 @@ router.post('/nginx/enable', async (req, res) => {
867
971
  try {
868
972
  const { filePath, name } = req.body;
869
973
  const enabledPath = `/etc/nginx/sites-enabled/${name}`;
870
- if (!fs.existsSync(enabledPath)) await execAsync(`sudo ln -s "${filePath}" "${enabledPath}"`);
974
+ if (!fs.existsSync(enabledPath)) await execAsync(`sudo ln -s "${filePath}" "${enabledPath}"`, { windowsHide: true });
871
975
  res.json({ success: true });
872
976
  } catch (err) {
873
977
  res.status(500).json({ error: err.message });
@@ -880,7 +984,7 @@ router.post('/nginx/disable', async (req, res) => {
880
984
  try {
881
985
  const { name } = req.body;
882
986
  const enabledPath = `/etc/nginx/sites-enabled/${name}`;
883
- if (fs.existsSync(enabledPath)) await execAsync(`sudo rm "${enabledPath}"`);
987
+ if (fs.existsSync(enabledPath)) await execAsync(`sudo rm "${enabledPath}"`, { windowsHide: true });
884
988
  res.json({ success: true });
885
989
  } catch (err) {
886
990
  res.status(500).json({ error: err.message });
@@ -925,7 +1029,7 @@ router.post('/nginx/action', async (req, res) => {
925
1029
 
926
1030
  try {
927
1031
  let cmd;
928
- const nginxCwd = isWindows ? 'C:\\nginx' : '/';
1032
+ const nginxCwd = isWindows ? path.dirname(NGINX_BIN) : undefined;
929
1033
 
930
1034
  if (isWindows) {
931
1035
  const bin = `"${NGINX_BIN}"`;
@@ -948,7 +1052,7 @@ router.post('/nginx/action', async (req, res) => {
948
1052
  cmd = cmdMap[action];
949
1053
  }
950
1054
 
951
- const { stdout, stderr } = await execAsync(cmd, { cwd: nginxCwd }).catch(err => ({
1055
+ const { stdout, stderr } = await execAsync(cmd, { cwd: nginxCwd, windowsHide: true }).catch(err => ({
952
1056
  stdout: err.stdout || '',
953
1057
  stderr: err.stderr || err.message,
954
1058
  }));
@@ -993,7 +1097,7 @@ router.get('/setup/info', (req, res) => {
993
1097
  const pathPresets = serverIsWindows
994
1098
  ? ['C:\\pm2me\\apps', 'C:\\Users\\apps', 'D:\\pm2me\\apps']
995
1099
  : ['/opt/pm2me/apps', '/home/apps', '/var/pm2me/apps'];
996
-
1100
+
997
1101
  // Read version from package.json
998
1102
  let version = 'unknown';
999
1103
  try {
@@ -1001,7 +1105,7 @@ router.get('/setup/info', (req, res) => {
1001
1105
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
1002
1106
  version = pkg.version || 'unknown';
1003
1107
  } catch { }
1004
-
1108
+
1005
1109
  res.json({
1006
1110
  os: serverIsWindows ? 'windows' : 'linux',
1007
1111
  isWindows: serverIsWindows,
@@ -1020,21 +1124,21 @@ router.post('/setup/complete', async (req, res) => {
1020
1124
  }
1021
1125
  // Hash with bcrypt (same as auth.js uses for verification)
1022
1126
  const passwordHash = await bcrypt.hash(adminPassword, 10);
1023
-
1127
+
1024
1128
  // Update db data
1025
1129
  db.data.admin = { passwordHash };
1026
1130
  db.data.settings = { ...db.data.settings, appsPath };
1027
1131
  db.data.setupComplete = true;
1028
-
1132
+
1029
1133
  // Force write to database
1030
1134
  await db.write();
1031
-
1135
+
1032
1136
  // Verify the data was written by reading it back
1033
1137
  await db.read();
1034
-
1138
+
1035
1139
  // Ensure apps dir exists
1036
1140
  fs.mkdirSync(appsPath, { recursive: true });
1037
-
1141
+
1038
1142
  console.log('Setup completed successfully. Database saved to:', db.data);
1039
1143
  res.json({ success: true });
1040
1144
  } catch (err) {
@@ -1051,218 +1155,50 @@ router.get('/system/version-check', async (req, res) => {
1051
1155
  try {
1052
1156
  // Read local version from root package.json (two levels up from backend/routes/)
1053
1157
  let localVersion = 'unknown';
1054
- let packageName = '@drocketxx/pm2me';
1055
1158
  try {
1056
1159
  const pkgPath = path.resolve(__dirname, '../../package.json');
1057
1160
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
1058
1161
  localVersion = pkg.version || 'unknown';
1059
- packageName = pkg.name || packageName;
1060
1162
  } catch { }
1061
1163
 
1062
- // Fetch latest version from npm registry
1164
+ // Fetch latest release from GitHub
1063
1165
  let latest = null, releaseUrl = null, changelog = '';
1064
1166
  try {
1065
- const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`, {
1167
+ const response = await fetch('https://api.github.com/repos/drocketxx/PM2Me/releases/latest', {
1066
1168
  headers: { 'User-Agent': 'pm2me-updater' }
1067
1169
  });
1068
1170
  if (response.ok) {
1069
1171
  const data = await response.json();
1070
- latest = data.version || null;
1071
- releaseUrl = data.homepage || `https://www.npmjs.com/package/${packageName}`;
1072
- changelog = data.description || '';
1172
+ latest = data.tag_name?.replace(/^v/, '') || null;
1173
+ releaseUrl = data.html_url || null;
1174
+ changelog = data.body || '';
1073
1175
  }
1074
- } catch (err) {
1075
- console.error('Failed to fetch npm version:', err);
1076
- }
1176
+ } catch { }
1077
1177
 
1078
- const hasUpdate = latest && localVersion !== 'unknown' && latest !== localVersion &&
1079
- latest.localeCompare(localVersion, undefined, { numeric: true, sensitivity: 'base' }) > 0;
1178
+ const hasUpdate = latest && localVersion !== 'unknown' && latest !== localVersion;
1080
1179
  res.json({ current: localVersion, latest, hasUpdate, releaseUrl, changelog });
1081
1180
  } catch (err) {
1082
1181
  res.status(500).json({ error: err.message });
1083
1182
  }
1084
1183
  });
1085
1184
 
1086
- // GET update log
1087
- router.get('/system/update-log', async (req, res) => {
1088
- try {
1089
- const logPath = isWindows ? path.join(os.tmpdir(), 'pm2me-update.log') : '/tmp/pm2me-update.log';
1090
- if (!fs.existsSync(logPath)) {
1091
- return res.json({ exists: false, content: '' });
1092
- }
1093
- const content = fs.readFileSync(logPath, 'utf8');
1094
- res.json({ exists: true, content });
1095
- } catch (err) {
1096
- res.status(500).json({ error: err.message });
1097
- }
1098
- });
1099
-
1100
1185
  router.post('/system/update', async (req, res) => {
1101
1186
  try {
1102
1187
  // Global npm install if installed globally, otherwise git pull + rebuild
1103
1188
  const isGlobal = !fs.existsSync(path.resolve(__dirname, '../../.git'));
1104
1189
  let cmd, cwd;
1105
1190
  if (isGlobal) {
1106
- // Create update script that runs independently
1107
- if (isWindows) {
1108
- // Windows: Create PowerShell script and execute with Start-Process
1109
- const scriptPath = path.join(os.tmpdir(), 'pm2me-update.ps1');
1110
- const logPath = path.join(os.tmpdir(), 'pm2me-update.log');
1111
- const scriptContent = `$LogPath = "${logPath}"
1112
- function Log($msg) {
1113
- $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
1114
- "[$timestamp] $msg" | Out-File -FilePath $LogPath -Append
1115
- }
1116
-
1117
- Log "PM2Me Update Script Started"
1118
- Start-Sleep -Seconds 3
1119
-
1120
- Log "Cleaning npm cache..."
1121
- npm cache clean --force 2>&1 | Out-File -FilePath $LogPath -Append
1122
-
1123
- Log "Installing latest version from npm..."
1124
- $installResult = npm install -g @drocketxx/pm2me@latest --force 2>&1
1125
- $installResult | Out-File -FilePath $LogPath -Append
1126
-
1127
- if ($LASTEXITCODE -eq 0) {
1128
- Log "Successfully installed latest version"
1129
- } else {
1130
- Log "ERROR: npm install failed"
1131
- exit 1
1132
- }
1133
-
1134
- Log "Uninstalling old service..."
1135
- pm2me service uninstall 2>&1 | Out-File -FilePath $LogPath -Append
1136
-
1137
- Start-Sleep -Seconds 3
1138
-
1139
- Log "Installing new service..."
1140
- $serviceResult = pm2me service install 2>&1
1141
- $serviceResult | Out-File -FilePath $LogPath -Append
1142
-
1143
- if ($LASTEXITCODE -eq 0) {
1144
- Log "Service installed successfully"
1145
- Log "Update completed successfully!"
1146
- } else {
1147
- Log "ERROR: Service install failed"
1148
- exit 1
1149
- }
1150
-
1151
- Remove-Item -Path "${scriptPath}" -Force`;
1152
- fs.writeFileSync(scriptPath, scriptContent);
1153
- cmd = `powershell -Command "Start-Process -FilePath 'powershell' -ArgumentList '-ExecutionPolicy','Bypass','-File','${scriptPath}' -WindowStyle Hidden"`;
1154
-
1155
- exec(cmd, { windowsHide: true });
1156
-
1157
- res.json({
1158
- success: true,
1159
- output: `Update scheduled. Service will restart in ~30 seconds.\nLog: ${logPath}`,
1160
- cmd
1161
- });
1162
- } else {
1163
- // Linux: Get full paths to npm, pm2me, and node before running detached
1164
- let npmPath, pm2mePath, nodePath, binDir;
1165
- try {
1166
- const npmResult = await execAsync('which npm');
1167
- npmPath = npmResult.stdout.trim();
1168
- binDir = path.dirname(npmPath); // e.g., /root/.nvm/versions/node/v22.21.1/bin
1169
- } catch {
1170
- npmPath = '/usr/bin/npm';
1171
- binDir = '/usr/bin';
1172
- }
1173
- try {
1174
- const pm2meResult = await execAsync('which pm2me');
1175
- pm2mePath = pm2meResult.stdout.trim();
1176
- } catch {
1177
- pm2mePath = 'pm2me';
1178
- }
1179
- try {
1180
- const nodeResult = await execAsync('which node');
1181
- nodePath = nodeResult.stdout.trim();
1182
- } catch {
1183
- nodePath = path.join(binDir, 'node'); // Same dir as npm
1184
- }
1185
-
1186
- const scriptPath = '/tmp/pm2me-update.sh';
1187
- const logPath = '/tmp/pm2me-update.log';
1188
- const scriptContent = `#!/bin/bash
1189
- # Add Node.js bin directory to PATH
1190
- export PATH="${binDir}:$PATH"
1191
-
1192
- exec > ${logPath} 2>&1
1193
- echo "[$(date)] PM2Me Update Script Started"
1194
- echo "[$(date)] PATH: $PATH"
1195
- echo "[$(date)] Using node: ${nodePath}"
1196
- echo "[$(date)] Using npm: ${npmPath}"
1197
- echo "[$(date)] Using pm2me: ${pm2mePath}"
1198
- sleep 3
1199
-
1200
- echo "[$(date)] Cleaning npm cache..."
1201
- ${npmPath} cache clean --force || echo "Cache clean failed (non-critical)"
1202
-
1203
- echo "[$(date)] Installing latest version from npm..."
1204
- if ${npmPath} install -g @drocketxx/pm2me@latest --force; then
1205
- echo "[$(date)] Successfully installed latest version"
1206
- else
1207
- echo "[$(date)] ERROR: npm install failed - trying with sudo..."
1208
- if sudo -n ${npmPath} install -g @drocketxx/pm2me@latest --force 2>/dev/null; then
1209
- echo "[$(date)] Successfully installed with sudo"
1210
- else
1211
- echo "[$(date)] FATAL: npm install failed. Update aborted."
1212
- exit 1
1213
- fi
1214
- fi
1215
-
1216
- echo "[$(date)] Uninstalling old service..."
1217
- ${pm2mePath} service uninstall || echo "Service uninstall failed (might not exist)"
1218
-
1219
- sleep 3
1220
-
1221
- echo "[$(date)] Installing new service..."
1222
- if ${pm2mePath} service install; then
1223
- echo "[$(date)] Service installed successfully"
1224
- else
1225
- echo "[$(date)] ERROR: Service install failed"
1226
- exit 1
1227
- fi
1228
-
1229
- echo "[$(date)] Update completed successfully!"
1230
- rm -f ${scriptPath}`;
1231
- fs.writeFileSync(scriptPath, scriptContent, { mode: 0o755 });
1232
-
1233
- // Try to use systemd-run first (most reliable), fallback to at, then to background process
1234
- try {
1235
- // Check if systemd-run is available
1236
- await execAsync('which systemd-run');
1237
- cmd = `systemd-run --user --on-active=3s ${scriptPath}`;
1238
- } catch {
1239
- try {
1240
- // Check if at is available
1241
- await execAsync('which at');
1242
- cmd = `echo "${scriptPath}" | at now + 3 seconds 2>&1`;
1243
- } catch {
1244
- // Fallback to background process with setsid (detach from session)
1245
- cmd = `(sleep 3 && ${scriptPath}) & disown`;
1246
- }
1247
- }
1248
-
1249
- exec(cmd, { shell: '/bin/bash', detached: true });
1250
-
1251
- res.json({
1252
- success: true,
1253
- output: `Update scheduled. Service will restart in ~30 seconds.\nBin Dir: ${binDir}\nNode: ${nodePath}\nNpm: ${npmPath}\nPm2me: ${pm2mePath}\nLog: ${logPath}`,
1254
- cmd
1255
- });
1256
- }
1191
+ cmd = 'npm install -g pm2me@latest';
1192
+ cwd = '/';
1257
1193
  } else {
1258
1194
  cmd = 'git pull origin main && npm run build';
1259
1195
  cwd = path.resolve(__dirname, '../..');
1260
- const { stdout, stderr } = await execAsync(cmd, { cwd, windowsHide: true }).catch(err => ({
1261
- stdout: '', stderr: err.message
1262
- }));
1263
- const output = [stdout, stderr].filter(Boolean).join('\n').trim();
1264
- res.json({ success: !stderr.includes('error'), output, cmd });
1265
1196
  }
1197
+ const { stdout, stderr } = await execAsync(cmd, { cwd, windowsHide: true }).catch(err => ({
1198
+ stdout: '', stderr: err.message
1199
+ }));
1200
+ const output = [stdout, stderr].filter(Boolean).join('\n').trim();
1201
+ res.json({ success: !stderr.includes('error'), output, cmd });
1266
1202
  } catch (err) {
1267
1203
  res.status(500).json({ error: err.message });
1268
1204
  }