@trentapps/manager-protocol 1.1.1

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 (151) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +446 -0
  3. package/dist/analyzers/ArchitectureDetector.d.ts +44 -0
  4. package/dist/analyzers/ArchitectureDetector.d.ts.map +1 -0
  5. package/dist/analyzers/ArchitectureDetector.js +218 -0
  6. package/dist/analyzers/ArchitectureDetector.js.map +1 -0
  7. package/dist/analyzers/CSSAnalyzer.d.ts +104 -0
  8. package/dist/analyzers/CSSAnalyzer.d.ts.map +1 -0
  9. package/dist/analyzers/CSSAnalyzer.js +578 -0
  10. package/dist/analyzers/CSSAnalyzer.js.map +1 -0
  11. package/dist/analyzers/index.d.ts +5 -0
  12. package/dist/analyzers/index.d.ts.map +1 -0
  13. package/dist/analyzers/index.js +5 -0
  14. package/dist/analyzers/index.js.map +1 -0
  15. package/dist/cli.d.ts +8 -0
  16. package/dist/cli.d.ts.map +1 -0
  17. package/dist/cli.js +174 -0
  18. package/dist/cli.js.map +1 -0
  19. package/dist/design-system/index.d.ts +6 -0
  20. package/dist/design-system/index.d.ts.map +1 -0
  21. package/dist/design-system/index.js +6 -0
  22. package/dist/design-system/index.js.map +1 -0
  23. package/dist/design-system/tokens.d.ts +106 -0
  24. package/dist/design-system/tokens.d.ts.map +1 -0
  25. package/dist/design-system/tokens.js +554 -0
  26. package/dist/design-system/tokens.js.map +1 -0
  27. package/dist/engine/AppMonitor.d.ts +162 -0
  28. package/dist/engine/AppMonitor.d.ts.map +1 -0
  29. package/dist/engine/AppMonitor.js +754 -0
  30. package/dist/engine/AppMonitor.js.map +1 -0
  31. package/dist/engine/AuditLogger.d.ts +138 -0
  32. package/dist/engine/AuditLogger.d.ts.map +1 -0
  33. package/dist/engine/AuditLogger.js +448 -0
  34. package/dist/engine/AuditLogger.js.map +1 -0
  35. package/dist/engine/GitHubApprovalManager.d.ts +106 -0
  36. package/dist/engine/GitHubApprovalManager.d.ts.map +1 -0
  37. package/dist/engine/GitHubApprovalManager.js +315 -0
  38. package/dist/engine/GitHubApprovalManager.js.map +1 -0
  39. package/dist/engine/RateLimiter.d.ts +79 -0
  40. package/dist/engine/RateLimiter.d.ts.map +1 -0
  41. package/dist/engine/RateLimiter.js +232 -0
  42. package/dist/engine/RateLimiter.js.map +1 -0
  43. package/dist/engine/RulesEngine.d.ts +77 -0
  44. package/dist/engine/RulesEngine.d.ts.map +1 -0
  45. package/dist/engine/RulesEngine.js +400 -0
  46. package/dist/engine/RulesEngine.js.map +1 -0
  47. package/dist/engine/TaskManager.d.ts +173 -0
  48. package/dist/engine/TaskManager.d.ts.map +1 -0
  49. package/dist/engine/TaskManager.js +678 -0
  50. package/dist/engine/TaskManager.js.map +1 -0
  51. package/dist/engine/index.d.ts +9 -0
  52. package/dist/engine/index.d.ts.map +1 -0
  53. package/dist/engine/index.js +9 -0
  54. package/dist/engine/index.js.map +1 -0
  55. package/dist/index.d.ts +21 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +29 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/rules/architecture.d.ts +9 -0
  60. package/dist/rules/architecture.d.ts.map +1 -0
  61. package/dist/rules/architecture.js +322 -0
  62. package/dist/rules/architecture.js.map +1 -0
  63. package/dist/rules/azure.d.ts +7 -0
  64. package/dist/rules/azure.d.ts.map +1 -0
  65. package/dist/rules/azure.js +138 -0
  66. package/dist/rules/azure.js.map +1 -0
  67. package/dist/rules/compliance.d.ts +9 -0
  68. package/dist/rules/compliance.d.ts.map +1 -0
  69. package/dist/rules/compliance.js +304 -0
  70. package/dist/rules/compliance.js.map +1 -0
  71. package/dist/rules/css.d.ts +10 -0
  72. package/dist/rules/css.d.ts.map +1 -0
  73. package/dist/rules/css.js +1239 -0
  74. package/dist/rules/css.js.map +1 -0
  75. package/dist/rules/flask.d.ts +7 -0
  76. package/dist/rules/flask.d.ts.map +1 -0
  77. package/dist/rules/flask.js +155 -0
  78. package/dist/rules/flask.js.map +1 -0
  79. package/dist/rules/index.d.ts +607 -0
  80. package/dist/rules/index.d.ts.map +1 -0
  81. package/dist/rules/index.js +401 -0
  82. package/dist/rules/index.js.map +1 -0
  83. package/dist/rules/ml-ai.d.ts +7 -0
  84. package/dist/rules/ml-ai.d.ts.map +1 -0
  85. package/dist/rules/ml-ai.js +150 -0
  86. package/dist/rules/ml-ai.js.map +1 -0
  87. package/dist/rules/operational.d.ts +9 -0
  88. package/dist/rules/operational.d.ts.map +1 -0
  89. package/dist/rules/operational.js +318 -0
  90. package/dist/rules/operational.js.map +1 -0
  91. package/dist/rules/security.d.ts +9 -0
  92. package/dist/rules/security.d.ts.map +1 -0
  93. package/dist/rules/security.js +287 -0
  94. package/dist/rules/security.js.map +1 -0
  95. package/dist/rules/storage.d.ts +7 -0
  96. package/dist/rules/storage.d.ts.map +1 -0
  97. package/dist/rules/storage.js +134 -0
  98. package/dist/rules/storage.js.map +1 -0
  99. package/dist/rules/stripe.d.ts +7 -0
  100. package/dist/rules/stripe.d.ts.map +1 -0
  101. package/dist/rules/stripe.js +140 -0
  102. package/dist/rules/stripe.js.map +1 -0
  103. package/dist/rules/testing.d.ts +7 -0
  104. package/dist/rules/testing.d.ts.map +1 -0
  105. package/dist/rules/testing.js +135 -0
  106. package/dist/rules/testing.js.map +1 -0
  107. package/dist/rules/ux.d.ts +9 -0
  108. package/dist/rules/ux.d.ts.map +1 -0
  109. package/dist/rules/ux.js +280 -0
  110. package/dist/rules/ux.js.map +1 -0
  111. package/dist/rules/websocket.d.ts +7 -0
  112. package/dist/rules/websocket.d.ts.map +1 -0
  113. package/dist/rules/websocket.js +136 -0
  114. package/dist/rules/websocket.js.map +1 -0
  115. package/dist/server.d.ts +49 -0
  116. package/dist/server.d.ts.map +1 -0
  117. package/dist/server.js +2330 -0
  118. package/dist/server.js.map +1 -0
  119. package/dist/supervisor/AgentSupervisor.d.ts +235 -0
  120. package/dist/supervisor/AgentSupervisor.d.ts.map +1 -0
  121. package/dist/supervisor/AgentSupervisor.js +596 -0
  122. package/dist/supervisor/AgentSupervisor.js.map +1 -0
  123. package/dist/supervisor/ManagedServerRegistry.d.ts +48 -0
  124. package/dist/supervisor/ManagedServerRegistry.d.ts.map +1 -0
  125. package/dist/supervisor/ManagedServerRegistry.js +145 -0
  126. package/dist/supervisor/ManagedServerRegistry.js.map +1 -0
  127. package/dist/supervisor/ProjectTracker.d.ts +188 -0
  128. package/dist/supervisor/ProjectTracker.d.ts.map +1 -0
  129. package/dist/supervisor/ProjectTracker.js +617 -0
  130. package/dist/supervisor/ProjectTracker.js.map +1 -0
  131. package/dist/supervisor/index.d.ts +6 -0
  132. package/dist/supervisor/index.d.ts.map +1 -0
  133. package/dist/supervisor/index.js +6 -0
  134. package/dist/supervisor/index.js.map +1 -0
  135. package/dist/types/index.d.ts +1176 -0
  136. package/dist/types/index.d.ts.map +1 -0
  137. package/dist/types/index.js +391 -0
  138. package/dist/types/index.js.map +1 -0
  139. package/dist/utils/errors.d.ts +86 -0
  140. package/dist/utils/errors.d.ts.map +1 -0
  141. package/dist/utils/errors.js +171 -0
  142. package/dist/utils/errors.js.map +1 -0
  143. package/dist/utils/index.d.ts +5 -0
  144. package/dist/utils/index.d.ts.map +1 -0
  145. package/dist/utils/index.js +5 -0
  146. package/dist/utils/index.js.map +1 -0
  147. package/dist/utils/shell.d.ts +22 -0
  148. package/dist/utils/shell.d.ts.map +1 -0
  149. package/dist/utils/shell.js +29 -0
  150. package/dist/utils/shell.js.map +1 -0
  151. package/package.json +63 -0
@@ -0,0 +1,754 @@
1
+ /**
2
+ * Enterprise Agent Supervisor - App Monitor
3
+ *
4
+ * Monitors production applications running on the server.
5
+ * Tracks online status, health checks, and process information.
6
+ */
7
+ import { exec } from 'child_process';
8
+ import { promisify } from 'util';
9
+ import * as http from 'http';
10
+ import * as fs from 'fs';
11
+ import * as path from 'path';
12
+ import { escapeForShell } from '../utils/shell.js';
13
+ import { auditLogger } from './AuditLogger.js';
14
+ const execAsync = promisify(exec);
15
+ export class AppMonitor {
16
+ apps = new Map();
17
+ statusHistory = new Map();
18
+ lastCheckResults = new Map();
19
+ checkIntervals = new Map();
20
+ prodBasePath;
21
+ maxHistoryEntries;
22
+ maxTotalHistoryEntries;
23
+ defaultCheckIntervalMs;
24
+ defaultTimeoutMs;
25
+ constructor(options = {}) {
26
+ this.prodBasePath = options.prodBasePath ?? '/mnt/prod';
27
+ this.maxHistoryEntries = options.maxHistoryEntries ?? 100; // Reduced from 1000
28
+ this.maxTotalHistoryEntries = options.maxTotalHistoryEntries ?? 10000; // Global cap
29
+ this.defaultCheckIntervalMs = options.defaultCheckIntervalMs ?? 30000;
30
+ this.defaultTimeoutMs = options.defaultTimeoutMs ?? 5000;
31
+ }
32
+ /**
33
+ * Generate a unique app ID
34
+ */
35
+ generateAppId() {
36
+ return `app_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
37
+ }
38
+ /**
39
+ * Add a new app to monitor
40
+ */
41
+ async addApp(config) {
42
+ // Validate path exists
43
+ const fullPath = config.path.startsWith('/')
44
+ ? config.path
45
+ : path.join(this.prodBasePath, config.path);
46
+ const pathExists = await this.checkPathExists(fullPath);
47
+ if (!pathExists) {
48
+ throw new Error(`App path does not exist: ${fullPath}`);
49
+ }
50
+ // Check if app with same name or port already exists
51
+ for (const [, app] of this.apps) {
52
+ if (app.name === config.name) {
53
+ throw new Error(`App with name "${config.name}" already exists`);
54
+ }
55
+ if (app.port === config.port) {
56
+ throw new Error(`App with port ${config.port} already exists (${app.name})`);
57
+ }
58
+ }
59
+ const now = new Date().toISOString();
60
+ const app = {
61
+ id: this.generateAppId(),
62
+ name: config.name,
63
+ path: fullPath,
64
+ port: config.port,
65
+ description: config.description,
66
+ healthEndpoint: config.healthEndpoint,
67
+ expectedResponseCode: config.expectedResponseCode ?? 200,
68
+ checkIntervalMs: config.checkIntervalMs ?? this.defaultCheckIntervalMs,
69
+ timeoutMs: config.timeoutMs ?? this.defaultTimeoutMs,
70
+ enabled: true,
71
+ tags: config.tags,
72
+ metadata: config.metadata,
73
+ createdAt: now
74
+ };
75
+ // Re-check for duplicates immediately before insertion to prevent race condition
76
+ // (another addApp() call could have completed between our initial check and now)
77
+ for (const [, existingApp] of this.apps) {
78
+ if (existingApp.name === config.name) {
79
+ throw new Error(`App with name "${config.name}" already exists (added concurrently)`);
80
+ }
81
+ if (existingApp.port === config.port) {
82
+ throw new Error(`App with port ${config.port} already exists (${existingApp.name}, added concurrently)`);
83
+ }
84
+ }
85
+ this.apps.set(app.id, app);
86
+ this.statusHistory.set(app.id, []);
87
+ // Start monitoring if autoStart is true (default)
88
+ if (config.autoStart !== false) {
89
+ this.startMonitoring(app.id);
90
+ }
91
+ // Do an initial check
92
+ await this.checkAppHealth(app.id);
93
+ return app;
94
+ }
95
+ /**
96
+ * Remove an app from monitoring
97
+ */
98
+ removeApp(appId) {
99
+ const app = this.apps.get(appId);
100
+ if (!app) {
101
+ return false;
102
+ }
103
+ // Stop monitoring interval
104
+ this.stopMonitoring(appId);
105
+ // Remove from all maps
106
+ this.apps.delete(appId);
107
+ this.statusHistory.delete(appId);
108
+ this.lastCheckResults.delete(appId);
109
+ return true;
110
+ }
111
+ /**
112
+ * Get an app by ID
113
+ */
114
+ getApp(appId) {
115
+ return this.apps.get(appId);
116
+ }
117
+ /**
118
+ * Get app by name
119
+ */
120
+ getAppByName(name) {
121
+ for (const [, app] of this.apps) {
122
+ if (app.name === name) {
123
+ return app;
124
+ }
125
+ }
126
+ return undefined;
127
+ }
128
+ /**
129
+ * Get all apps
130
+ */
131
+ getAllApps() {
132
+ return Array.from(this.apps.values());
133
+ }
134
+ /**
135
+ * Update an app configuration
136
+ */
137
+ updateApp(appId, updates) {
138
+ const app = this.apps.get(appId);
139
+ if (!app) {
140
+ return undefined;
141
+ }
142
+ const updatedApp = {
143
+ ...app,
144
+ ...updates,
145
+ updatedAt: new Date().toISOString()
146
+ };
147
+ this.apps.set(appId, updatedApp);
148
+ // Restart monitoring if interval changed
149
+ if (updates.checkIntervalMs !== undefined && updates.checkIntervalMs !== app.checkIntervalMs) {
150
+ this.stopMonitoring(appId);
151
+ if (updatedApp.enabled) {
152
+ this.startMonitoring(appId);
153
+ }
154
+ }
155
+ return updatedApp;
156
+ }
157
+ /**
158
+ * Check if a path exists
159
+ */
160
+ async checkPathExists(pathToCheck) {
161
+ try {
162
+ await fs.promises.access(pathToCheck, fs.constants.F_OK);
163
+ return true;
164
+ }
165
+ catch {
166
+ return false;
167
+ }
168
+ }
169
+ /**
170
+ * Check if a port is in use
171
+ */
172
+ async isPortInUse(port) {
173
+ try {
174
+ const { stdout } = await execAsync(`lsof -i :${escapeForShell(port)} -t 2>/dev/null || true`);
175
+ const pid = stdout.trim();
176
+ if (pid) {
177
+ return { inUse: true, pid: parseInt(pid.split('\n')[0], 10) };
178
+ }
179
+ return { inUse: false };
180
+ }
181
+ catch (error) {
182
+ await auditLogger.log({
183
+ eventType: 'system_event',
184
+ action: 'port_check_failed',
185
+ outcome: 'failure',
186
+ details: {
187
+ port,
188
+ error: error instanceof Error ? error.message : String(error)
189
+ }
190
+ });
191
+ return { inUse: false };
192
+ }
193
+ }
194
+ /**
195
+ * Get process info by PID
196
+ */
197
+ async getProcessInfo(pid) {
198
+ try {
199
+ // Get memory and CPU using ps
200
+ const { stdout } = await execAsync(`ps -p ${escapeForShell(pid)} -o %mem,%cpu,etime --no-headers 2>/dev/null || true`);
201
+ const parts = stdout.trim().split(/\s+/);
202
+ if (parts.length >= 3) {
203
+ // Get total memory to calculate MB
204
+ const { stdout: memTotal } = await execAsync(`free -m | awk '/Mem:/ {print $2}'`);
205
+ const totalMemMb = parseInt(memTotal.trim(), 10);
206
+ const memPercent = parseFloat(parts[0]);
207
+ return {
208
+ memoryUsageMb: Math.round((memPercent / 100) * totalMemMb),
209
+ cpuPercent: parseFloat(parts[1]),
210
+ uptime: parts[2]
211
+ };
212
+ }
213
+ }
214
+ catch (error) {
215
+ await auditLogger.log({
216
+ eventType: 'system_event',
217
+ action: 'process_info_failed',
218
+ outcome: 'failure',
219
+ details: {
220
+ pid,
221
+ error: error instanceof Error ? error.message : String(error)
222
+ }
223
+ });
224
+ }
225
+ return {};
226
+ }
227
+ /**
228
+ * Make an HTTP health check request with proper cleanup to prevent memory leaks
229
+ */
230
+ async httpHealthCheck(port, endpoint, timeoutMs, expectedStatusCode) {
231
+ const startTime = Date.now();
232
+ const url = `http://localhost:${port}${endpoint}`;
233
+ return new Promise((resolve) => {
234
+ let resolved = false;
235
+ const safeResolve = (result) => {
236
+ if (!resolved) {
237
+ resolved = true;
238
+ resolve(result);
239
+ }
240
+ };
241
+ const request = http.get(url, { timeout: timeoutMs }, (res) => {
242
+ const responseTimeMs = Date.now() - startTime;
243
+ const success = res.statusCode === expectedStatusCode;
244
+ // Consume response body to prevent memory leak
245
+ res.resume();
246
+ // Clean up response listeners
247
+ res.on('end', () => {
248
+ safeResolve({
249
+ success,
250
+ statusCode: res.statusCode,
251
+ responseTimeMs,
252
+ error: success ? undefined : `Expected ${expectedStatusCode}, got ${res.statusCode}`
253
+ });
254
+ });
255
+ res.on('error', () => {
256
+ // Response error after connection established
257
+ request.destroy();
258
+ });
259
+ });
260
+ request.on('error', (err) => {
261
+ request.destroy();
262
+ safeResolve({
263
+ success: false,
264
+ responseTimeMs: Date.now() - startTime,
265
+ error: err.message
266
+ });
267
+ });
268
+ request.on('timeout', () => {
269
+ request.destroy();
270
+ safeResolve({
271
+ success: false,
272
+ responseTimeMs: timeoutMs,
273
+ error: 'Request timed out'
274
+ });
275
+ });
276
+ });
277
+ }
278
+ /**
279
+ * Check health of a specific app
280
+ */
281
+ async checkAppHealth(appId) {
282
+ const app = this.apps.get(appId);
283
+ if (!app) {
284
+ throw new Error(`App not found: ${appId}`);
285
+ }
286
+ const now = new Date().toISOString();
287
+ const portCheck = await this.isPortInUse(app.port);
288
+ let status = 'unknown';
289
+ let responseTimeMs;
290
+ let httpStatusCode;
291
+ let errorMessage;
292
+ let processInfo;
293
+ if (!portCheck.inUse) {
294
+ status = 'offline';
295
+ errorMessage = `Port ${app.port} is not listening`;
296
+ }
297
+ else {
298
+ // Get process info
299
+ if (portCheck.pid) {
300
+ processInfo = {
301
+ pid: portCheck.pid,
302
+ ...(await this.getProcessInfo(portCheck.pid))
303
+ };
304
+ }
305
+ // If health endpoint is configured, do HTTP check
306
+ if (app.healthEndpoint) {
307
+ const healthResult = await this.httpHealthCheck(app.port, app.healthEndpoint, app.timeoutMs, app.expectedResponseCode);
308
+ responseTimeMs = healthResult.responseTimeMs;
309
+ httpStatusCode = healthResult.statusCode;
310
+ if (healthResult.success) {
311
+ // Check if response time is degraded (> 2 seconds)
312
+ status = responseTimeMs > 2000 ? 'degraded' : 'online';
313
+ }
314
+ else {
315
+ status = 'degraded';
316
+ errorMessage = healthResult.error;
317
+ }
318
+ }
319
+ else {
320
+ // No health endpoint, just check port
321
+ status = 'online';
322
+ }
323
+ }
324
+ const result = {
325
+ appId: app.id,
326
+ appName: app.name,
327
+ status,
328
+ port: app.port,
329
+ path: app.path,
330
+ responseTimeMs,
331
+ httpStatusCode,
332
+ errorMessage,
333
+ checkedAt: now,
334
+ processInfo
335
+ };
336
+ // Store result
337
+ this.lastCheckResults.set(appId, result);
338
+ // Add to history
339
+ this.addToHistory(appId, {
340
+ appId,
341
+ status,
342
+ timestamp: now,
343
+ responseTimeMs,
344
+ errorMessage
345
+ });
346
+ return result;
347
+ }
348
+ /**
349
+ * Check health of all apps
350
+ */
351
+ async checkAllApps() {
352
+ const results = [];
353
+ for (const [appId, app] of this.apps) {
354
+ if (app.enabled) {
355
+ try {
356
+ const result = await this.checkAppHealth(appId);
357
+ results.push(result);
358
+ }
359
+ catch (error) {
360
+ await auditLogger.log({
361
+ eventType: 'system_event',
362
+ action: 'app_health_check_failed',
363
+ outcome: 'failure',
364
+ details: {
365
+ appId,
366
+ appName: app.name,
367
+ port: app.port,
368
+ path: app.path,
369
+ error: error instanceof Error ? error.message : String(error)
370
+ }
371
+ });
372
+ results.push({
373
+ appId,
374
+ appName: app.name,
375
+ status: 'unknown',
376
+ port: app.port,
377
+ path: app.path,
378
+ errorMessage: error instanceof Error ? error.message : String(error),
379
+ checkedAt: new Date().toISOString()
380
+ });
381
+ }
382
+ }
383
+ }
384
+ return results;
385
+ }
386
+ /**
387
+ * Get the last check result for an app
388
+ */
389
+ getLastCheckResult(appId) {
390
+ return this.lastCheckResults.get(appId);
391
+ }
392
+ /**
393
+ * Get status history for an app
394
+ */
395
+ getStatusHistory(appId, limit) {
396
+ const history = this.statusHistory.get(appId) ?? [];
397
+ if (limit) {
398
+ return history.slice(-limit);
399
+ }
400
+ return history;
401
+ }
402
+ /**
403
+ * Add entry to status history with global memory budget enforcement
404
+ */
405
+ addToHistory(appId, entry) {
406
+ let history = this.statusHistory.get(appId);
407
+ if (!history) {
408
+ history = [];
409
+ this.statusHistory.set(appId, history);
410
+ }
411
+ history.push(entry);
412
+ // Trim per-app history if too long
413
+ if (history.length > this.maxHistoryEntries) {
414
+ history.splice(0, history.length - this.maxHistoryEntries);
415
+ }
416
+ // Enforce global memory budget
417
+ this.enforceGlobalHistoryLimit();
418
+ }
419
+ /**
420
+ * Enforce global history limit to prevent unbounded memory growth
421
+ * Uses LRU-style eviction: removes oldest entries from largest histories first
422
+ */
423
+ enforceGlobalHistoryLimit() {
424
+ let totalEntries = 0;
425
+ for (const history of this.statusHistory.values()) {
426
+ totalEntries += history.length;
427
+ }
428
+ // If under limit, no action needed
429
+ if (totalEntries <= this.maxTotalHistoryEntries) {
430
+ return;
431
+ }
432
+ // Calculate how many entries to remove (10% of excess for efficiency)
433
+ const entriesToRemove = Math.ceil((totalEntries - this.maxTotalHistoryEntries) * 1.1);
434
+ let removed = 0;
435
+ // Sort apps by history size (largest first) for LRU-style eviction
436
+ const appHistories = Array.from(this.statusHistory.entries())
437
+ .sort((a, b) => b[1].length - a[1].length);
438
+ // Remove from largest histories first
439
+ for (const [appId, history] of appHistories) {
440
+ if (removed >= entriesToRemove) {
441
+ break;
442
+ }
443
+ // Remove oldest 10% or 1 entry, whichever is larger
444
+ const toRemoveFromThis = Math.max(1, Math.floor(history.length * 0.1));
445
+ const actualRemoved = Math.min(toRemoveFromThis, entriesToRemove - removed);
446
+ history.splice(0, actualRemoved);
447
+ removed += actualRemoved;
448
+ // Clean up empty histories
449
+ if (history.length === 0) {
450
+ this.statusHistory.delete(appId);
451
+ }
452
+ }
453
+ if (removed > 0) {
454
+ console.log(`[AppMonitor] Enforced global history limit: removed ${removed} entries across ${appHistories.length} apps`);
455
+ }
456
+ }
457
+ /**
458
+ * Start monitoring an app
459
+ */
460
+ startMonitoring(appId) {
461
+ const app = this.apps.get(appId);
462
+ if (!app) {
463
+ return false;
464
+ }
465
+ // Clear any existing interval
466
+ this.stopMonitoring(appId);
467
+ // Set up new interval
468
+ const interval = setInterval(async () => {
469
+ try {
470
+ await this.checkAppHealth(appId);
471
+ }
472
+ catch (error) {
473
+ await auditLogger.log({
474
+ eventType: 'system_event',
475
+ action: 'monitoring_check_failed',
476
+ outcome: 'failure',
477
+ details: {
478
+ appId,
479
+ appName: app.name,
480
+ error: error instanceof Error ? error.message : String(error)
481
+ }
482
+ });
483
+ }
484
+ }, app.checkIntervalMs);
485
+ this.checkIntervals.set(appId, interval);
486
+ return true;
487
+ }
488
+ /**
489
+ * Stop monitoring an app
490
+ */
491
+ stopMonitoring(appId) {
492
+ const interval = this.checkIntervals.get(appId);
493
+ if (interval) {
494
+ clearInterval(interval);
495
+ this.checkIntervals.delete(appId);
496
+ return true;
497
+ }
498
+ return false;
499
+ }
500
+ /**
501
+ * Enable/disable an app
502
+ */
503
+ setAppEnabled(appId, enabled) {
504
+ const app = this.apps.get(appId);
505
+ if (!app) {
506
+ return false;
507
+ }
508
+ app.enabled = enabled;
509
+ app.updatedAt = new Date().toISOString();
510
+ if (enabled) {
511
+ this.startMonitoring(appId);
512
+ }
513
+ else {
514
+ this.stopMonitoring(appId);
515
+ }
516
+ return true;
517
+ }
518
+ /**
519
+ * Get monitoring statistics
520
+ */
521
+ getStats() {
522
+ let onlineApps = 0;
523
+ let offlineApps = 0;
524
+ let degradedApps = 0;
525
+ let unknownApps = 0;
526
+ let totalResponseTime = 0;
527
+ let responseTimeCount = 0;
528
+ let lastFullCheckAt;
529
+ for (const [appId] of this.apps) {
530
+ const result = this.lastCheckResults.get(appId);
531
+ if (result) {
532
+ if (!lastFullCheckAt || result.checkedAt > lastFullCheckAt) {
533
+ lastFullCheckAt = result.checkedAt;
534
+ }
535
+ switch (result.status) {
536
+ case 'online':
537
+ onlineApps++;
538
+ break;
539
+ case 'offline':
540
+ offlineApps++;
541
+ break;
542
+ case 'degraded':
543
+ degradedApps++;
544
+ break;
545
+ default:
546
+ unknownApps++;
547
+ }
548
+ if (result.responseTimeMs !== undefined) {
549
+ totalResponseTime += result.responseTimeMs;
550
+ responseTimeCount++;
551
+ }
552
+ }
553
+ else {
554
+ unknownApps++;
555
+ }
556
+ }
557
+ return {
558
+ totalApps: this.apps.size,
559
+ onlineApps,
560
+ offlineApps,
561
+ degradedApps,
562
+ unknownApps,
563
+ averageResponseTimeMs: responseTimeCount > 0
564
+ ? Math.round(totalResponseTime / responseTimeCount)
565
+ : undefined,
566
+ lastFullCheckAt
567
+ };
568
+ }
569
+ /**
570
+ * Find apps by tag
571
+ */
572
+ findAppsByTag(tag) {
573
+ return Array.from(this.apps.values()).filter(app => app.tags?.includes(tag));
574
+ }
575
+ /**
576
+ * Find apps by status
577
+ */
578
+ findAppsByStatus(status) {
579
+ const results = [];
580
+ for (const [appId, app] of this.apps) {
581
+ const lastResult = this.lastCheckResults.get(appId);
582
+ if (lastResult?.status === status) {
583
+ results.push(app);
584
+ }
585
+ }
586
+ return results;
587
+ }
588
+ /**
589
+ * Get apps that are currently offline
590
+ */
591
+ getOfflineApps() {
592
+ return this.findAppsByStatus('offline');
593
+ }
594
+ /**
595
+ * Get apps that are degraded
596
+ */
597
+ getDegradedApps() {
598
+ return this.findAppsByStatus('degraded');
599
+ }
600
+ /**
601
+ * Scan /mnt/prod for potential apps (directories with package.json or similar)
602
+ */
603
+ async scanForApps() {
604
+ const results = [];
605
+ try {
606
+ const entries = await fs.promises.readdir(this.prodBasePath, { withFileTypes: true });
607
+ for (const entry of entries) {
608
+ if (entry.isDirectory()) {
609
+ const appPath = path.join(this.prodBasePath, entry.name);
610
+ const packageJsonPath = path.join(appPath, 'package.json');
611
+ let hasPackageJson = false;
612
+ let appType = 'unknown';
613
+ const potentialPorts = [];
614
+ try {
615
+ await fs.promises.access(packageJsonPath, fs.constants.F_OK);
616
+ hasPackageJson = true;
617
+ // Try to read package.json for port info
618
+ const packageJson = JSON.parse(await fs.promises.readFile(packageJsonPath, 'utf-8'));
619
+ // Detect app type
620
+ if (packageJson.dependencies?.next) {
621
+ appType = 'nextjs';
622
+ potentialPorts.push(3000);
623
+ }
624
+ else if (packageJson.dependencies?.express) {
625
+ appType = 'express';
626
+ potentialPorts.push(3000);
627
+ }
628
+ else if (packageJson.dependencies?.fastify) {
629
+ appType = 'fastify';
630
+ potentialPorts.push(3000);
631
+ }
632
+ else if (packageJson.dependencies?.['@nestjs/core']) {
633
+ appType = 'nestjs';
634
+ potentialPorts.push(3000);
635
+ }
636
+ else {
637
+ appType = 'node';
638
+ }
639
+ // Check scripts for port mentions
640
+ if (packageJson.scripts) {
641
+ const scriptStr = JSON.stringify(packageJson.scripts);
642
+ const portMatches = scriptStr.match(/PORT[=:]\s*(\d+)/gi);
643
+ if (portMatches) {
644
+ for (const match of portMatches) {
645
+ const port = parseInt(match.replace(/PORT[=:]\s*/i, ''), 10);
646
+ if (!potentialPorts.includes(port)) {
647
+ potentialPorts.push(port);
648
+ }
649
+ }
650
+ }
651
+ }
652
+ }
653
+ catch (err) {
654
+ // Distinguish ENOENT (expected) from other errors (unexpected)
655
+ if (err.code !== 'ENOENT') {
656
+ console.warn(`[AppMonitor] Unexpected error reading package.json for ${entry.name}:`, err.message);
657
+ }
658
+ // Check for other app types
659
+ const dockerfilePath = path.join(appPath, 'Dockerfile');
660
+ const composePath = path.join(appPath, 'docker-compose.yml');
661
+ try {
662
+ await fs.promises.access(dockerfilePath, fs.constants.F_OK);
663
+ appType = 'docker';
664
+ }
665
+ catch (dockerErr) {
666
+ if (dockerErr.code !== 'ENOENT') {
667
+ console.warn(`[AppMonitor] Unexpected error checking Dockerfile for ${entry.name}:`, dockerErr.message);
668
+ }
669
+ try {
670
+ await fs.promises.access(composePath, fs.constants.F_OK);
671
+ appType = 'docker-compose';
672
+ }
673
+ catch (composeErr) {
674
+ if (composeErr.code !== 'ENOENT') {
675
+ console.warn(`[AppMonitor] Unexpected error checking docker-compose.yml for ${entry.name}:`, composeErr.message);
676
+ }
677
+ // Unknown type - this is expected for directories without recognized app files
678
+ }
679
+ }
680
+ }
681
+ results.push({
682
+ name: entry.name,
683
+ path: appPath,
684
+ type: appType,
685
+ hasPackageJson,
686
+ potentialPorts
687
+ });
688
+ }
689
+ }
690
+ }
691
+ catch (error) {
692
+ // Directory might not exist
693
+ console.error(`Error scanning ${this.prodBasePath}:`, error);
694
+ }
695
+ return results;
696
+ }
697
+ /**
698
+ * Get logs for an app (tail of stdout/stderr or PM2 logs)
699
+ */
700
+ async getAppLogs(appId, lines = 50) {
701
+ const app = this.apps.get(appId);
702
+ if (!app) {
703
+ throw new Error(`App not found: ${appId}`);
704
+ }
705
+ // Try to get logs from PM2 first
706
+ try {
707
+ const { stdout } = await execAsync(`pm2 logs ${escapeForShell(app.name)} --lines ${escapeForShell(lines)} --nostream 2>/dev/null`);
708
+ return { logs: stdout, source: 'pm2' };
709
+ }
710
+ catch {
711
+ // PM2 not available or app not managed by PM2
712
+ }
713
+ // Try to find log files in app directory
714
+ const possibleLogPaths = [
715
+ path.join(app.path, 'logs', 'app.log'),
716
+ path.join(app.path, 'logs', 'error.log'),
717
+ path.join(app.path, 'log', 'app.log'),
718
+ path.join(app.path, 'app.log'),
719
+ path.join(app.path, '.log'),
720
+ `/var/log/${app.name}.log`
721
+ ];
722
+ for (const logPath of possibleLogPaths) {
723
+ try {
724
+ const { stdout } = await execAsync(`tail -n ${escapeForShell(lines)} ${escapeForShell(logPath)} 2>/dev/null`);
725
+ return { logs: stdout, source: logPath };
726
+ }
727
+ catch {
728
+ // Try next path
729
+ }
730
+ }
731
+ // Try journalctl if it's a systemd service
732
+ try {
733
+ const { stdout } = await execAsync(`journalctl -u ${escapeForShell(app.name)} -n ${escapeForShell(lines)} --no-pager 2>/dev/null`);
734
+ if (stdout.trim()) {
735
+ return { logs: stdout, source: 'journalctl' };
736
+ }
737
+ }
738
+ catch {
739
+ // Not a systemd service
740
+ }
741
+ return { logs: 'No logs found', source: 'none' };
742
+ }
743
+ /**
744
+ * Cleanup - stop all monitoring intervals
745
+ */
746
+ cleanup() {
747
+ for (const [appId] of this.checkIntervals) {
748
+ this.stopMonitoring(appId);
749
+ }
750
+ }
751
+ }
752
+ // Default instance
753
+ export const appMonitor = new AppMonitor();
754
+ //# sourceMappingURL=AppMonitor.js.map