@drocketxx/pm2me 1.1.12 → 1.1.14

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 (69) hide show
  1. package/backend/db/index.js +6 -0
  2. package/backend/routes/api.js +14 -2
  3. package/backend/services/systemService.js +50 -0
  4. package/package.json +1 -1
  5. package/apps/Delete-Test-App/README.md +0 -1
  6. package/apps/Delete-Test-App/dist/server.js +0 -57
  7. package/apps/Delete-Test-App/package.json +0 -19
  8. package/apps/Delete-Test-App/src/server.ts +0 -33
  9. package/apps/Delete-Test-App/tsconfig.json +0 -15
  10. package/apps/Delete-Test-App-2/README.md +0 -1
  11. package/apps/Delete-Test-App-2/dist/server.js +0 -57
  12. package/apps/Delete-Test-App-2/package.json +0 -19
  13. package/apps/Delete-Test-App-2/src/server.ts +0 -33
  14. package/apps/Delete-Test-App-2/tsconfig.json +0 -15
  15. package/apps/Delete-Test-App-2_deploy.log +0 -12
  16. package/apps/Delete-Test-App_deploy.log +0 -12
  17. package/apps/PM2Me-main/README.md +0 -236
  18. package/apps/PM2Me-main/backend/app.js +0 -109
  19. package/apps/PM2Me-main/backend/db/index.js +0 -40
  20. package/apps/PM2Me-main/backend/nodemon.json +0 -10
  21. package/apps/PM2Me-main/backend/package.json +0 -31
  22. package/apps/PM2Me-main/backend/public/assets/index-DZ4rSpP9.css +0 -1
  23. package/apps/PM2Me-main/backend/public/assets/index-KLmI9qSM.js +0 -13
  24. package/apps/PM2Me-main/backend/public/icon.png +0 -0
  25. package/apps/PM2Me-main/backend/public/index.html +0 -14
  26. package/apps/PM2Me-main/backend/public/vite.svg +0 -1
  27. package/apps/PM2Me-main/backend/routes/api.js +0 -932
  28. package/apps/PM2Me-main/backend/routes/auth.js +0 -44
  29. package/apps/PM2Me-main/backend/services/gitService.js +0 -110
  30. package/apps/PM2Me-main/backend/services/notificationService.js +0 -48
  31. package/apps/PM2Me-main/backend/services/pm2Service.js +0 -84
  32. package/apps/PM2Me-main/backend/services/systemService.js +0 -113
  33. package/apps/PM2Me-main/c:VsCodemyPM2Metmp_old_dashboard.vue +0 -390
  34. package/apps/PM2Me-main/package.json +0 -21
  35. package/apps/PM2Me-main/screenshot/001.png +0 -0
  36. package/apps/PM2Me-main/test_db.js +0 -8
  37. package/apps/PM2Me-main/tmp_old_dashboard.vue +0 -390
  38. package/apps/PM2Me-main_deploy.log +0 -25
  39. package/apps/PM2Me-test/README.md +0 -1
  40. package/apps/PM2Me-test/dist/server.js +0 -63
  41. package/apps/PM2Me-test/package.json +0 -19
  42. package/apps/PM2Me-test/src/server.ts +0 -33
  43. package/apps/PM2Me-test/tsconfig.json +0 -15
  44. package/apps/PM2Me-test-main/README.md +0 -1
  45. package/apps/PM2Me-test-main/dist/server.js +0 -63
  46. package/apps/PM2Me-test-main/package.json +0 -19
  47. package/apps/PM2Me-test-main/src/server.ts +0 -33
  48. package/apps/PM2Me-test-main/tsconfig.json +0 -15
  49. package/apps/PM2Me-test-main_deploy.log +0 -24
  50. package/apps/PM2Me-test-uat/README.md +0 -1
  51. package/apps/PM2Me-test-uat/dist/server.js +0 -63
  52. package/apps/PM2Me-test-uat/package.json +0 -19
  53. package/apps/PM2Me-test-uat/src/server.ts +0 -33
  54. package/apps/PM2Me-test-uat/tsconfig.json +0 -15
  55. package/apps/PM2Me-test-uat-updated/README.md +0 -1
  56. package/apps/PM2Me-test-uat-updated/dist/server.js +0 -63
  57. package/apps/PM2Me-test-uat-updated/package.json +0 -19
  58. package/apps/PM2Me-test-uat-updated/src/server.ts +0 -33
  59. package/apps/PM2Me-test-uat-updated/tsconfig.json +0 -15
  60. package/apps/PM2Me-test-uat-updated_deploy.log +0 -24
  61. package/apps/PM2Me-test-uat2/README.md +0 -1
  62. package/apps/PM2Me-test-uat2/dist/server.js +0 -63
  63. package/apps/PM2Me-test-uat2/package.json +0 -19
  64. package/apps/PM2Me-test-uat2/src/server.ts +0 -33
  65. package/apps/PM2Me-test-uat2/tsconfig.json +0 -15
  66. package/apps/PM2Me-test-uat2_deploy.log +0 -25
  67. package/apps/PM2Me-test-uat_deploy.log +0 -24
  68. package/apps/PM2Me-test_deploy.log +0 -24
  69. package/apps/PM2Me-test_health.log +0 -4
@@ -1,932 +0,0 @@
1
- import express from 'express';
2
- import db from '../db/index.js';
3
- import * as pm2Service from '../services/pm2Service.js';
4
- import * as gitService from '../services/gitService.js';
5
- import * as notificationService from '../services/notificationService.js';
6
- import * as systemService from '../services/systemService.js';
7
- import path from 'path';
8
- import { fileURLToPath } from 'url';
9
- import fs from 'fs';
10
- import crypto from 'crypto';
11
- import { exec } from 'child_process';
12
- import util from 'util';
13
-
14
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
- const workspacesDir = path.resolve(__dirname, '../../apps');
16
-
17
- const router = express.Router();
18
-
19
- // GET all PM2 apps
20
- router.get('/pm2/list', async (req, res) => {
21
- try {
22
- await pm2Service.connectPM2();
23
- const list = await pm2Service.listApps();
24
- res.json(list);
25
- } catch (err) {
26
- res.status(500).json({ error: err.message });
27
- }
28
- });
29
-
30
- // GET system stats
31
- router.get('/system/stats', async (req, res) => {
32
- try {
33
- const stats = await systemService.getSystemStats();
34
- res.json(stats);
35
- } catch (err) {
36
- res.status(500).json({ error: err.message });
37
- }
38
- });
39
-
40
- // GET PM2 version and update check
41
- const execAsync = util.promisify(exec);
42
- router.get('/pm2/version-check', async (req, res) => {
43
- try {
44
- // Check globally installed PM2 version via npm (avoids local node_modules/.bin/pm2)
45
- let installedVersion = null;
46
- try {
47
- const { stdout } = await execAsync('npm list -g pm2 --depth=0 --json');
48
- const parsed = JSON.parse(stdout);
49
- installedVersion = parsed?.dependencies?.pm2?.version || null;
50
- } catch {
51
- // not installed globally
52
- }
53
-
54
- if (!installedVersion) {
55
- return res.json({ installed: false });
56
- }
57
-
58
- // Get latest version from npm registry
59
- let latestVersion = null;
60
- try {
61
- const { stdout } = await execAsync('npm view pm2 version');
62
- latestVersion = stdout.trim();
63
- } catch {
64
- latestVersion = null;
65
- }
66
-
67
- const hasUpdate = latestVersion && installedVersion !== latestVersion &&
68
- latestVersion.localeCompare(installedVersion, undefined, { numeric: true, sensitivity: 'base' }) > 0;
69
-
70
- res.json({ installed: true, version: installedVersion, latestVersion, hasUpdate });
71
- } catch (err) {
72
- res.status(500).json({ error: err.message });
73
- }
74
- });
75
-
76
- // POST PM2 install or update
77
- router.post('/pm2/install-update', async (req, res) => {
78
- try {
79
- res.writeHead(200, { 'Content-Type': 'text/plain', 'Transfer-Encoding': 'chunked' });
80
- const child = exec('npm install -g pm2@latest');
81
- child.stdout.on('data', (d) => res.write(d));
82
- child.stderr.on('data', (d) => res.write(d));
83
- child.on('close', (code) => {
84
- res.write(code === 0 ? '\n✅ Done!' : '\n❌ Failed.');
85
- res.end();
86
- });
87
- } catch (err) {
88
- res.status(500).json({ error: err.message });
89
- }
90
- });
91
-
92
- // PM2 Action: start, stop, restart, delete
93
- router.post('/pm2/:action', async (req, res) => {
94
- const { action } = req.params;
95
- const { nameOrId } = req.body;
96
- try {
97
- await pm2Service.connectPM2();
98
- let result;
99
- if (action === 'start') result = await pm2Service.startApp(nameOrId);
100
- else if (action === 'stop') result = await pm2Service.stopApp(nameOrId);
101
- else if (action === 'restart') result = await pm2Service.restartApp(nameOrId);
102
- else if (action === 'reload') result = await pm2Service.reloadApp(nameOrId);
103
- else if (action === 'delete') result = await pm2Service.deleteApp(nameOrId);
104
-
105
- // Find app by name to emit system log
106
- const app = db.data.apps.find(a => a.name === nameOrId);
107
- if (app && req.io) {
108
- const actionLabel = action.charAt(0).toUpperCase() + action.slice(1);
109
- req.io.emit(`deploy-log-${app.id}`, `[pm2me] ${actionLabel}`);
110
- }
111
-
112
- res.json({ success: true, result });
113
- } catch (err) {
114
- res.status(500).json({ error: err.message });
115
- }
116
- });
117
-
118
- // App Config CRUD (LowDB)
119
- router.get('/apps', (req, res) => {
120
- res.json(db.data.apps);
121
- });
122
-
123
- router.post('/apps', async (req, res) => {
124
- const newApp = { id: Date.now().toString(), ...req.body, status: 'created' };
125
- db.data.apps.push(newApp);
126
- await db.write();
127
- res.json(newApp);
128
- });
129
-
130
- router.delete('/apps/:id', async (req, res) => {
131
- const { id } = req.params;
132
- db.data.apps = db.data.apps.filter(app => app.id !== id);
133
- await db.write();
134
- res.json({ success: true });
135
- });
136
-
137
- router.get('/apps/:id/sync-status', async (req, res) => {
138
- const { id } = req.params;
139
- const appConfig = db.data.apps.find(a => a.id === id);
140
- if (!appConfig) return res.status(404).json({ error: 'App not found' });
141
-
142
- const targetPath = path.join(workspacesDir, appConfig.name);
143
- try {
144
- const behindCount = await gitService.getBehindCount(appConfig.repoUrl, targetPath, appConfig.branch, appConfig.token);
145
- res.json({ behindCount });
146
- } catch (err) {
147
- res.status(500).json({ error: err.message });
148
- }
149
- });
150
-
151
- router.put('/apps/:id', async (req, res) => {
152
- const { id } = req.params;
153
- const index = db.data.apps.findIndex(app => app.id === id);
154
- if (index === -1) return res.status(404).json({ error: 'App not found' });
155
-
156
- // Merge new config, but preserve id and status
157
- db.data.apps[index] = {
158
- ...db.data.apps[index],
159
- ...req.body,
160
- id: db.data.apps[index].id,
161
- status: db.data.apps[index].status
162
- };
163
- await db.write();
164
- res.json(db.data.apps[index]);
165
- });
166
-
167
- // Get Git branches
168
- router.post('/git/branches', async (req, res) => {
169
- const { repoUrl, token } = req.body;
170
- try {
171
- const branches = await gitService.getBranches(repoUrl, token);
172
- res.json(branches);
173
- } catch (err) {
174
- res.status(500).json({ error: err.message });
175
- }
176
- });
177
-
178
- // Verify and Add git token
179
- router.post('/git/accounts', async (req, res) => {
180
- const { token } = req.body;
181
- if (!token) return res.status(400).json({ success: false, error: 'No token provided' });
182
- try {
183
- const response = await fetch('https://api.github.com/user', {
184
- headers: {
185
- 'Authorization': `token ${token}`,
186
- 'User-Agent': 'PM2Me-App'
187
- }
188
- });
189
- if (response.ok) {
190
- const data = await response.json();
191
- // Check if already added
192
- if (!db.data.githubAccounts) db.data.githubAccounts = [];
193
- const exists = db.data.githubAccounts.find(a => a.username === data.login);
194
- if (exists) return res.status(400).json({ success: false, error: 'Account already connected' });
195
-
196
- const newAccount = {
197
- id: data.id.toString(),
198
- username: data.login,
199
- avatarUrl: data.avatar_url,
200
- token: token
201
- };
202
- db.data.githubAccounts.push(newAccount);
203
- await db.write();
204
-
205
- res.json({ success: true, account: newAccount });
206
- } else {
207
- res.status(401).json({ success: false, error: 'Invalid token' });
208
- }
209
- } catch (err) {
210
- res.status(500).json({ error: err.message });
211
- }
212
- });
213
-
214
- // Remove a github account
215
- router.delete('/git/accounts/:id', async (req, res) => {
216
- const { id } = req.params;
217
- if (db.data.githubAccounts) {
218
- db.data.githubAccounts = db.data.githubAccounts.filter(a => a.id !== id);
219
- await db.write();
220
- }
221
- res.json({ success: true });
222
- });
223
-
224
- // Get all connected accounts
225
- router.get('/git/accounts', (req, res) => {
226
- res.json(db.data.githubAccounts || []);
227
- });
228
-
229
- // Fetch all repositories from connected accounts
230
- router.get('/git/repositories', async (req, res) => {
231
- const accounts = db.data.githubAccounts || [];
232
- const groupedRepos = [];
233
-
234
- try {
235
- for (const account of accounts) {
236
- const response = await fetch(`https://api.github.com/user/repos?per_page=100&sort=updated`, {
237
- headers: {
238
- 'Authorization': `token ${account.token}`,
239
- 'User-Agent': 'PM2Me-App'
240
- }
241
- });
242
- if (response.ok) {
243
- const repos = await response.json();
244
- const mappedRepos = repos.map(r => ({
245
- id: r.id,
246
- name: r.name,
247
- fullName: r.full_name,
248
- cloneUrl: r.clone_url,
249
- private: r.private
250
- }));
251
- groupedRepos.push({
252
- account: account.username,
253
- accountId: account.id,
254
- token: account.token,
255
- avatarUrl: account.avatarUrl,
256
- repositories: mappedRepos
257
- });
258
- }
259
- }
260
- res.json(groupedRepos);
261
- } catch (err) {
262
- res.status(500).json({ error: err.message });
263
- }
264
- });
265
-
266
- const rollback = async (appId, appConfig, io, targetPath, lastStep, logProcess, setPipelineState, logFilePath) => {
267
- if (!appConfig.lastSuccessfulCommitHash || appConfig.lastSuccessfulCommitHash === appConfig.commitHash) {
268
- logProcess('No stable version to rollback to or already on last good version.', true);
269
- return false;
270
- }
271
-
272
- logProcess(`Initiating Auto-Rollback to last successful commit: ${appConfig.lastSuccessfulCommitHash}`, true);
273
- try {
274
- const failedHash = appConfig.commitHash;
275
- const failedMsg = appConfig.commitMessage;
276
-
277
- // 1. Checkout last good commit
278
- const restored = await gitService.checkout(targetPath, appConfig.lastSuccessfulCommitHash);
279
-
280
- // 2. Restore successful configuration
281
- appConfig.pm2Script = appConfig.lastSuccessfulPm2Script;
282
- appConfig.env = JSON.parse(JSON.stringify(appConfig.lastSuccessfulEnv || {}));
283
-
284
- // 3. Re-setup environment files
285
- const envFileData = Object.entries(appConfig.env || {})
286
- .map(([k, v]) => `${k}=${v}`)
287
- .join('\n');
288
- fs.writeFileSync(path.join(targetPath, '.env'), envFileData);
289
-
290
- // 4. Re-run Build if necessary
291
- if (appConfig.buildScript) {
292
- logProcess('Re-building original working version...', true);
293
- const normalizedScript = appConfig.buildScript.split('\n')
294
- .map(s => s.trim())
295
- .filter(Boolean)
296
- .join(' && ');
297
- await new Promise((resolve, reject) => {
298
- const child = exec(normalizedScript, { cwd: targetPath, maxBuffer: 10 * 1024 * 1024 });
299
- child.stdout.on('data', data => { io.emit(`deploy-log-${appId}`, data.toString()); fs.appendFileSync(logFilePath, data); });
300
- child.stderr.on('data', data => { io.emit(`deploy-log-${appId}`, data.toString()); fs.appendFileSync(logFilePath, data); });
301
- child.on('close', code => code !== 0 ? reject(new Error('Rollback build failed')) : resolve());
302
- });
303
- }
304
-
305
- // 5. Restart PM2 with original version
306
- logProcess('Restarting PM2 with original version...', true);
307
- await pm2Service.connectPM2();
308
- const startOpts = {
309
- name: appConfig.name,
310
- cwd: targetPath,
311
- script: appConfig.pm2Script || 'npm',
312
- updateEnv: true,
313
- env: appConfig.env || {}
314
- };
315
-
316
- try {
317
- await pm2Service.reloadApp(appConfig.name, startOpts);
318
- } catch (e) {
319
- try { await pm2Service.deleteApp(appConfig.name); } catch (d) { }
320
- await pm2Service.startApp(startOpts);
321
- }
322
-
323
- // 6. Update DB to reflect rollback state
324
- appConfig.commitHash = restored.hash;
325
- appConfig.commitMessage = restored.message;
326
- appConfig.rollbackOccurred = true;
327
- appConfig.failedCommitHash = failedHash;
328
- appConfig.failedCommitMessage = failedMsg;
329
- appConfig.status = 'running';
330
- await setPipelineState(`failed:${lastStep}`);
331
-
332
- logProcess(`Rollback successful. System is running on commit: ${restored.message}`, true);
333
- await db.write();
334
- return true;
335
- } catch (rollbackErr) {
336
- const rbErrorMsg = rollbackErr.message || util.inspect(rollbackErr);
337
- logProcess(`Critical: Rollback failed: ${rbErrorMsg}`, true);
338
- return false;
339
- }
340
- };
341
-
342
- // Helper to normalize Git repo URLs for comparison
343
- const normalizeRepoUrl = (url) => {
344
- if (!url) return '';
345
- return url.replace(/\.git$/, '').replace(/\/$/, '').toLowerCase();
346
- };
347
-
348
- export const performDeployment = async (appId, io) => {
349
- const appConfig = db.data.apps.find(a => a.id === appId);
350
- if (!appConfig) throw new Error('App not found');
351
-
352
- const targetPath = path.join(workspacesDir, appConfig.name);
353
- const logFilePath = path.join(workspacesDir, `${appConfig.name}_deploy.log`);
354
- let lastStep = 'pulling';
355
-
356
- if (!fs.existsSync(workspacesDir)) {
357
- fs.mkdirSync(workspacesDir, { recursive: true });
358
- }
359
-
360
- fs.writeFileSync(logFilePath, `--- Deployment Started at ${new Date().toISOString()} ---\n`);
361
-
362
- const logProcess = (msg, isSystem = false) => {
363
- const formattedMsg = isSystem ? `[pm2me] ${msg}` : msg;
364
- console.log(`[Deploy ${appConfig.name}]`, formattedMsg);
365
- io.emit(`deploy-log-${appId}`, formattedMsg + '\n');
366
- fs.appendFileSync(logFilePath, formattedMsg + '\n');
367
- };
368
-
369
- const setPipelineState = async (state) => {
370
- appConfig.pipelineState = state;
371
- await db.write();
372
- io.emit(`pipeline-state-${appId}`, state);
373
- console.log(`[Pipeline ${appConfig.name}]`, state);
374
- };
375
-
376
- try {
377
- appConfig.status = 'deploying';
378
- await db.write();
379
- logProcess('Sync', true);
380
-
381
- lastStep = 'pulling';
382
- await setPipelineState('pulling');
383
- logProcess('Synchronizing Repository', true);
384
- const { message: syncMsg, commitHash, commitMessage } = await gitService.syncRepo(appConfig.repoUrl, targetPath, appConfig.branch, appConfig.token);
385
- appConfig.commitHash = commitHash;
386
- appConfig.commitMessage = commitMessage;
387
- logProcess(`Git: ${syncMsg} on branch ${appConfig.branch} (Commit: ${commitMessage})`);
388
-
389
- logProcess('Setting up env', true);
390
- const envFileData = Object.entries(appConfig.env || {})
391
- .map(([k, v]) => `${k}=${v}`)
392
- .join('\n');
393
- fs.writeFileSync(path.join(targetPath, '.env'), envFileData);
394
-
395
- if (appConfig.buildScript) {
396
- const normalizedScript = appConfig.buildScript.split('\n')
397
- .map(s => s.trim())
398
- .filter(Boolean)
399
- .join(' && ');
400
-
401
- lastStep = 'building';
402
- await setPipelineState('building');
403
- logProcess('Executing Build Script', true);
404
- await new Promise((resolve, reject) => {
405
- const child = exec(normalizedScript, { cwd: targetPath, maxBuffer: 10 * 1024 * 1024 });
406
- child.stdout.on('data', data => { io.emit(`deploy-log-${appId}`, data.toString()); fs.appendFileSync(logFilePath, data); });
407
- child.stderr.on('data', data => { io.emit(`deploy-log-${appId}`, data.toString()); fs.appendFileSync(logFilePath, data); });
408
- child.on('close', code => {
409
- if (code !== 0) reject(new Error(`Build script exited with code ${code}`));
410
- else { logProcess('Build Script Completed', true); resolve(); }
411
- });
412
- child.on('error', err => { reject(err); });
413
- });
414
- }
415
-
416
- lastStep = 'starting';
417
- await setPipelineState('starting');
418
- logProcess('Starting PM2', true);
419
- await pm2Service.connectPM2();
420
-
421
- let pmArgs = ['start'];
422
- if (appConfig.pm2Args) pmArgs = appConfig.pm2Args.split(' ').filter(Boolean);
423
-
424
- let startOptions = {
425
- name: appConfig.name,
426
- cwd: targetPath,
427
- script: appConfig.pm2Script || 'npm',
428
- args: appConfig.pm2Script === 'npm' ? pmArgs : [],
429
- env: appConfig.env || {}
430
- };
431
-
432
- if (appConfig.ecosystemFile) startOptions = path.join(targetPath, appConfig.ecosystemFile);
433
-
434
- logProcess('Updating Application in PM2', true);
435
- let exists = null;
436
- try {
437
- const list = await pm2Service.listApps();
438
- exists = list.find(a => a.name === appConfig.name);
439
- if (exists) {
440
- const updateOptions = { ...startOptions, updateEnv: true };
441
- if (appConfig.zeroDowntime !== false) {
442
- logProcess('Zero-downtime Reloading with new environment', true);
443
- await pm2Service.reloadApp(appConfig.name, updateOptions);
444
- } else {
445
- logProcess('Restarting (Non Zero-downtime) with new environment', true);
446
- await pm2Service.restartApp(appConfig.name, updateOptions);
447
- }
448
- } else {
449
- logProcess('Fresh Starting', true);
450
- await pm2Service.startApp(startOptions);
451
- }
452
- } catch (e) {
453
- const errorMsg = e.message || util.inspect(e);
454
- logProcess(`Action failed: ${errorMsg}. Falling back to clean start...`, true);
455
- try { await pm2Service.deleteApp(appConfig.name); } catch (delErr) { }
456
- await pm2Service.startApp(startOptions);
457
- }
458
-
459
- appConfig.status = 'running';
460
- await setPipelineState('online');
461
-
462
- // Stabilization checks
463
- const stabilizationTime = 10000; // 10 seconds
464
-
465
- // Capture initial restarts AFTER the reload/restart, so we have a clean baseline
466
- const freshList = await pm2Service.listApps();
467
- const freshApp = freshList.find(a => a.name === appConfig.name);
468
- const initialRestarts = freshApp ? (freshApp.pm2_env ? freshApp.pm2_env.restart_time : 0) : 0;
469
-
470
- logProcess(`Stabilization started. Verifying health for ${stabilizationTime / 1000}s...`, true);
471
-
472
- // Finalize success after X seconds
473
- setTimeout(async () => {
474
- try {
475
- // Re-find in case db memory changed or reloaded
476
- const currentApp = db.data.apps.find(a => a.id === appId);
477
- if (!currentApp) return;
478
-
479
- await pm2Service.connectPM2();
480
- const list = await pm2Service.listApps();
481
- const pmApp = list.find(a => a.name === currentApp.name);
482
-
483
- if (pmApp) {
484
- const status = pmApp.pm2_env.status;
485
- const restarts = pmApp.pm2_env.restart_time;
486
-
487
- if (status === 'online' && restarts === initialRestarts) {
488
- // Success! Update markers
489
- currentApp.lastSuccessfulCommitHash = currentApp.commitHash;
490
- currentApp.lastSuccessfulCommitMessage = currentApp.commitMessage;
491
- currentApp.lastSuccessfulPm2Script = currentApp.pm2Script;
492
- currentApp.lastSuccessfulEnv = JSON.parse(JSON.stringify(currentApp.env || {}));
493
- currentApp.rollbackOccurred = false;
494
- currentApp.failedCommitHash = null;
495
- currentApp.failedCommitMessage = null;
496
- await db.write();
497
- logProcess('Stabilization complete. Commit marked as successful.', true);
498
- } else {
499
- logProcess(`App unhealthy after ${stabilizationTime / 1000}s (Status: ${status}, Restarts: ${restarts} vs ${initialRestarts}). Rolling back...`, true);
500
- await rollback(appId, currentApp, io, targetPath, 'online', logProcess, setPipelineState, logFilePath);
501
- }
502
- }
503
- } catch (err) {
504
- console.error(`Health check failed for ${appConfig.name}:`, err);
505
- }
506
- }, stabilizationTime);
507
-
508
- logProcess('Deployment Online (Health period active)', true);
509
- await db.write();
510
- await notificationService.notifyAll(appConfig.name, 'success');
511
- } catch (err) {
512
- const errorMsg = err.message || util.inspect(err);
513
- logProcess(`Deployment failed: ${errorMsg}`, true);
514
-
515
- // Auto-Rollback Logic
516
- if (lastStep === 'building' || lastStep === 'starting') {
517
- const rolledBack = await rollback(appId, appConfig, io, targetPath, lastStep, logProcess, setPipelineState, logFilePath);
518
- if (rolledBack) return;
519
- }
520
-
521
- appConfig.status = 'failed';
522
- await setPipelineState(`failed:${lastStep}`);
523
- await db.write();
524
- await notificationService.notifyAll(appConfig.name, 'failed', errorMsg);
525
- throw err;
526
- }
527
- };
528
-
529
- // Build and Deploy an App
530
- router.post('/deploy/:appId', async (req, res) => {
531
- try {
532
- await performDeployment(req.params.appId, req.io);
533
- res.json({ success: true });
534
- } catch (err) {
535
- res.status(500).json({ error: err.message });
536
- }
537
- });
538
-
539
- // Get Deploy Logs for an App
540
- router.get('/deploy/:appId/logs', async (req, res) => {
541
- const { appId } = req.params;
542
- const appConfig = db.data.apps.find(a => a.id === appId);
543
- if (!appConfig) return res.status(404).json({ error: 'App not found' });
544
-
545
- const logFilePath = path.join(workspacesDir, `${appConfig.name}_deploy.log`);
546
- let logs = '';
547
-
548
- if (fs.existsSync(logFilePath)) {
549
- logs += fs.readFileSync(logFilePath, 'utf8');
550
- }
551
-
552
- try {
553
- await pm2Service.connectPM2();
554
- const list = await pm2Service.listApps();
555
- const pmApp = list.find(app => app.name === appConfig.name);
556
- if (pmApp && pmApp.pm2_env) {
557
- const outPath = pmApp.pm2_env.pm_out_log_path;
558
- const errPath = pmApp.pm2_env.pm_err_log_path;
559
-
560
- const readTailLines = (filePath, prefix = '', maxLines = 100) => {
561
- if (!filePath || !fs.existsSync(filePath)) return '';
562
- const stats = fs.statSync(filePath);
563
- const size = stats.size;
564
- const maxBytes = 50000;
565
- const start = Math.max(0, size - maxBytes);
566
- const buffer = Buffer.alloc(Math.max(0, size - start));
567
- if (buffer.length === 0) return '';
568
- const fd = fs.openSync(filePath, 'r');
569
- fs.readSync(fd, buffer, 0, buffer.length, start);
570
- fs.closeSync(fd);
571
- const text = buffer.toString('utf8');
572
- const lines = text.split('\n').filter(l => l.trim());
573
- return lines.slice(-maxLines).map(line => `${prefix} | ${line}`).join('\n');
574
- };
575
-
576
- const outLogs = readTailLines(outPath, `[out]`);
577
- const errLogs = readTailLines(errPath, `[err]`);
578
-
579
- if (outLogs || errLogs) {
580
- logs += '\n\n--- 🔵 PM2 Execution Logs ---\n';
581
- if (errLogs) logs += errLogs + '\n';
582
- if (outLogs) logs += outLogs + '\n';
583
- }
584
- }
585
- } catch (err) {
586
- console.error('Failed to fetch PM2 logs', err);
587
- }
588
-
589
- res.send(logs);
590
- });
591
-
592
- // GitHub Webhook Receiver
593
- router.post('/webhook', async (req, res) => {
594
- const payload = req.body;
595
- const signature = req.headers['x-hub-signature-256'];
596
- const deliveryId = req.headers['x-github-delivery'];
597
- const eventType = req.headers['x-github-event'] || 'unknown';
598
- const { webhookSecret } = db.data.settings;
599
-
600
- const logEntry = {
601
- id: crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(),
602
- timestamp: new Date().toISOString(),
603
- eventType,
604
- deliveryId,
605
- repository: payload.repository?.full_name || 'unknown',
606
- status: 'pending',
607
- details: ''
608
- };
609
-
610
- const addLog = async (entry) => {
611
- if (!db.data.webhookLogs) db.data.webhookLogs = [];
612
- db.data.webhookLogs.unshift(entry);
613
- if (db.data.webhookLogs.length > 50) {
614
- db.data.webhookLogs = db.data.webhookLogs.slice(0, 50);
615
- }
616
- await db.write();
617
- if (req.io) {
618
- req.io.emit('webhook-log', entry);
619
- }
620
- };
621
-
622
- // Signature verification using captured raw body
623
- if (webhookSecret && signature && req.rawBody) {
624
- try {
625
- const hmac = crypto.createHmac('sha256', webhookSecret);
626
- const digest = 'sha256=' + hmac.update(req.rawBody).digest('hex');
627
- if (signature !== digest) {
628
- logEntry.status = 'error';
629
- logEntry.details = 'Signature mismatch';
630
- await addLog(logEntry);
631
- return res.status(401).send('Signature mismatch');
632
- }
633
- } catch (e) {
634
- console.error('Webhook verification error:', e);
635
- logEntry.status = 'error';
636
- logEntry.details = `Verification failed: ${e.message}`;
637
- await addLog(logEntry);
638
- return res.status(500).send('Internal Error during verification');
639
- }
640
- }
641
-
642
- if (eventType === 'push') {
643
- const repoUrl = normalizeRepoUrl(payload.repository?.clone_url);
644
- const branch = payload.ref?.replace('refs/heads/', '');
645
-
646
- const appsToDeploy = db.data.apps.filter(a =>
647
- normalizeRepoUrl(a.repoUrl) === repoUrl &&
648
- a.branch === branch &&
649
- a.autoSync === true
650
- );
651
-
652
- if (appsToDeploy.length === 0) {
653
- logEntry.status = 'ignored';
654
- logEntry.details = `No matching apps for repo: ${repoUrl}, branch: ${branch}`;
655
- await addLog(logEntry);
656
- return res.status(200).send('No matching apps');
657
- }
658
-
659
- logEntry.status = 'success';
660
- logEntry.details = `Triggered deployment for ${appsToDeploy.length} apps: ${appsToDeploy.map(a => a.name).join(', ')}`;
661
- await addLog(logEntry);
662
-
663
- res.status(202).send('Accepted');
664
-
665
- for (const app of appsToDeploy) {
666
- try {
667
- await notificationService.notifyAll(app.name, 'Push event received, triggering auto-sync...');
668
- // Trigger deployment directly
669
- performDeployment(app.id, req.io).catch(err => {
670
- console.error(`Auto-Sync failed for ${app.name}:`, err);
671
- });
672
- } catch (e) {
673
- console.error(e);
674
- }
675
- }
676
- } else if (eventType === 'ping') {
677
- logEntry.status = 'success';
678
- logEntry.details = 'Webhook ping received and verified';
679
- await addLog(logEntry);
680
- res.status(200).send('PONG');
681
- } else {
682
- logEntry.status = 'ignored';
683
- logEntry.details = `Event type ${eventType} not processed`;
684
- await addLog(logEntry);
685
- res.status(200).send('Event not processed');
686
- }
687
- });
688
-
689
- // Settings endpoints
690
- router.get('/settings', (req, res) => {
691
- res.json(db.data.settings);
692
- });
693
-
694
- router.post('/settings', async (req, res) => {
695
- db.data.settings = { ...db.data.settings, ...req.body };
696
- await db.write();
697
- res.json({ success: true });
698
- });
699
-
700
- router.get('/settings/webhook-logs', (req, res) => {
701
- res.json(db.data.webhookLogs || []);
702
- });
703
-
704
- // ─── Nginx Management ────────────────────────────────────────────────────────
705
- import os from 'os';
706
-
707
- const isWindows = os.platform() === 'win32';
708
- const NGINX_CONF = isWindows ? 'C:\\nginx\\conf\\nginx.conf' : '/etc/nginx/nginx.conf';
709
- const NGINX_CONF_DIR = isWindows ? 'C:\\nginx\\conf' : '/etc/nginx';
710
- const NGINX_BIN = isWindows ? 'C:\\nginx\\nginx.exe' : 'nginx';
711
-
712
- // GET nginx info (os, paths, status)
713
- router.get('/nginx/info', async (req, res) => {
714
- try {
715
- const info = {
716
- os: isWindows ? 'windows' : 'linux',
717
- confFile: NGINX_CONF,
718
- confDir: NGINX_CONF_DIR,
719
- nginxBin: NGINX_BIN,
720
- };
721
-
722
- // Check if nginx binary exists / is installed
723
- try {
724
- const checkCmd = isWindows
725
- ? `"${NGINX_BIN}" -v`
726
- : 'nginx -v';
727
- const { stderr } = await execAsync(checkCmd);
728
- const match = (stderr || '').match(/nginx\/([\\d.]+)/);
729
- info.version = match ? match[1] : 'unknown';
730
- info.installed = true;
731
- } catch {
732
- info.installed = false;
733
- }
734
-
735
- res.json(info);
736
- } catch (err) {
737
- res.status(500).json({ error: err.message });
738
- }
739
- });
740
-
741
- // GET nginx status (running or not)
742
- router.get('/nginx/status', async (req, res) => {
743
- try {
744
- let running = false;
745
- try {
746
- if (isWindows) {
747
- const { stdout } = await execAsync('tasklist /FI "IMAGENAME eq nginx.exe" /NH');
748
- running = stdout.toLowerCase().includes('nginx.exe');
749
- } else {
750
- const { stdout } = await execAsync('pgrep -x nginx');
751
- running = stdout.trim().length > 0;
752
- }
753
- } catch {
754
- running = false;
755
- }
756
- res.json({ running });
757
- } catch (err) {
758
- res.status(500).json({ error: err.message });
759
- }
760
- });
761
-
762
- // GET nginx config file content
763
- router.get('/nginx/config', async (req, res) => {
764
- try {
765
- const filePath = req.query.file || NGINX_CONF;
766
- if (!fs.existsSync(filePath)) {
767
- return res.json({ content: '', exists: false, path: filePath });
768
- }
769
- const content = fs.readFileSync(filePath, 'utf8');
770
- res.json({ content, exists: true, path: filePath });
771
- } catch (err) {
772
- res.status(500).json({ error: err.message });
773
- }
774
- });
775
-
776
- // POST save nginx config file
777
- router.post('/nginx/config', async (req, res) => {
778
- try {
779
- const { content, file } = req.body;
780
- const filePath = file || NGINX_CONF;
781
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
782
- fs.writeFileSync(filePath, content, 'utf8');
783
- res.json({ success: true });
784
- } catch (err) {
785
- res.status(500).json({ error: err.message });
786
- }
787
- });
788
-
789
- // GET list all nginx config files (conf dir + sites-available + conf.d)
790
- router.get('/nginx/files', async (req, res) => {
791
- try {
792
- const files = [];
793
- if (isWindows) {
794
- const confDir = 'C:\\nginx\\conf';
795
- if (fs.existsSync(confDir)) {
796
- for (const f of fs.readdirSync(confDir)) {
797
- if (f.endsWith('.conf')) {
798
- files.push({ name: f, path: path.join(confDir, f), dir: confDir, enabled: true, enabledPath: null });
799
- }
800
- }
801
- }
802
- } else {
803
- const enabledDir = '/etc/nginx/sites-enabled';
804
- const enabledSet = new Set();
805
- if (fs.existsSync(enabledDir)) {
806
- for (const f of fs.readdirSync(enabledDir)) enabledSet.add(f);
807
- }
808
- const addFromDir = (dir) => {
809
- if (!fs.existsSync(dir)) return;
810
- for (const f of fs.readdirSync(dir)) {
811
- const fullPath = path.join(dir, f);
812
- try {
813
- const stat = fs.lstatSync(fullPath);
814
- if (stat.isFile()) {
815
- files.push({ name: f, path: fullPath, dir, enabled: enabledSet.has(f), enabledPath: path.join(enabledDir, f) });
816
- }
817
- } catch { }
818
- }
819
- };
820
- if (fs.existsSync('/etc/nginx/nginx.conf')) {
821
- files.push({ name: 'nginx.conf', path: '/etc/nginx/nginx.conf', dir: '/etc/nginx', enabled: true, enabledPath: null });
822
- }
823
- addFromDir('/etc/nginx/sites-available');
824
- addFromDir('/etc/nginx/conf.d');
825
- }
826
- res.json({ files, isWindows });
827
- } catch (err) {
828
- res.status(500).json({ error: err.message });
829
- }
830
- });
831
-
832
- // POST enable a site (create symlink sites-available → sites-enabled)
833
- router.post('/nginx/enable', async (req, res) => {
834
- if (isWindows) return res.json({ success: true, message: 'N/A on Windows' });
835
- try {
836
- const { filePath, name } = req.body;
837
- const enabledPath = `/etc/nginx/sites-enabled/${name}`;
838
- if (!fs.existsSync(enabledPath)) await execAsync(`sudo ln -s "${filePath}" "${enabledPath}"`);
839
- res.json({ success: true });
840
- } catch (err) {
841
- res.status(500).json({ error: err.message });
842
- }
843
- });
844
-
845
- // POST disable a site (remove symlink from sites-enabled)
846
- router.post('/nginx/disable', async (req, res) => {
847
- if (isWindows) return res.json({ success: true, message: 'N/A on Windows' });
848
- try {
849
- const { name } = req.body;
850
- const enabledPath = `/etc/nginx/sites-enabled/${name}`;
851
- if (fs.existsSync(enabledPath)) await execAsync(`sudo rm "${enabledPath}"`);
852
- res.json({ success: true });
853
- } catch (err) {
854
- res.status(500).json({ error: err.message });
855
- }
856
- });
857
-
858
- // POST create new nginx config file
859
- router.post('/nginx/files/new', async (req, res) => {
860
- try {
861
- const { name } = req.body;
862
- const dir = isWindows ? 'C:\\nginx\\conf' : '/etc/nginx/sites-available';
863
- const fname = name.endsWith('.conf') ? name : name + '.conf';
864
- const filePath = path.join(dir, fname);
865
- if (fs.existsSync(filePath)) return res.status(409).json({ error: 'File already exists' });
866
- fs.mkdirSync(dir, { recursive: true });
867
- fs.writeFileSync(filePath, `# ${fname}\n\nserver {\n listen 80;\n server_name example.com;\n\n location / {\n root /var/www/html;\n index index.html;\n }\n}\n`, 'utf8');
868
- res.json({ success: true, path: filePath, name: fname });
869
- } catch (err) {
870
- res.status(500).json({ error: err.message });
871
- }
872
- });
873
-
874
- // DELETE nginx config file
875
- router.delete('/nginx/files', async (req, res) => {
876
- try {
877
- const { filePath } = req.body;
878
- if (!filePath) return res.status(400).json({ error: 'filePath required' });
879
- if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
880
- res.json({ success: true });
881
- } catch (err) {
882
- res.status(500).json({ error: err.message });
883
- }
884
- });
885
-
886
- // POST nginx action: test | start | reload | stop | quit
887
- router.post('/nginx/action', async (req, res) => {
888
- const { action } = req.body;
889
- const validActions = ['test', 'start', 'reload', 'stop', 'quit'];
890
- if (!validActions.includes(action)) {
891
- return res.status(400).json({ error: 'Invalid action' });
892
- }
893
-
894
- try {
895
- let cmd;
896
- const nginxCwd = isWindows ? 'C:\\nginx' : '/';
897
-
898
- if (isWindows) {
899
- const bin = `"${NGINX_BIN}"`;
900
- const cmdMap = {
901
- test: `${bin} -t -c "${NGINX_CONF}"`,
902
- start: `powershell -Command "Start-Process -FilePath '${NGINX_BIN}' -WorkingDirectory 'C:\\nginx' -WindowStyle Hidden"`,
903
- reload: `${bin} -s reload`,
904
- stop: `${bin} -s stop`,
905
- quit: `${bin} -s quit`,
906
- };
907
- cmd = cmdMap[action];
908
- } else {
909
- const cmdMap = {
910
- test: `sudo nginx -t`,
911
- start: `sudo nginx`,
912
- reload: `sudo nginx -s reload`,
913
- stop: `sudo nginx -s stop`,
914
- quit: `sudo nginx -s quit`,
915
- };
916
- cmd = cmdMap[action];
917
- }
918
-
919
- const { stdout, stderr } = await execAsync(cmd, { cwd: nginxCwd }).catch(err => ({
920
- stdout: err.stdout || '',
921
- stderr: err.stderr || err.message,
922
- }));
923
-
924
- const output = [stdout, stderr].filter(Boolean).join('\n').trim();
925
- const success = !output.toLowerCase().includes('failed') && !output.toLowerCase().includes('error:');
926
- res.json({ success, output });
927
- } catch (err) {
928
- res.status(500).json({ error: err.message });
929
- }
930
- });
931
-
932
- export default router;