@monodog/backend 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/.eslintrc.cjs +15 -0
  2. package/dist/cli.js +98 -0
  3. package/dist/gitService.js +240 -0
  4. package/dist/index.js +1185 -0
  5. package/dist/utils/helpers.js +198 -0
  6. package/package.json +41 -0
  7. package/prisma/migrations/20251017041048_init/migration.sql +19 -0
  8. package/prisma/migrations/20251017083007_add_package/migration.sql +21 -0
  9. package/prisma/migrations/20251021083705_alter_package/migration.sql +37 -0
  10. package/prisma/migrations/20251022085155_test/migration.sql +2 -0
  11. package/prisma/migrations/20251022160841_/migration.sql +35 -0
  12. package/prisma/migrations/20251023130158_rename_column_name/migration.sql +34 -0
  13. package/prisma/migrations/20251023174837_/migration.sql +34 -0
  14. package/prisma/migrations/20251023175830_uodate_schema/migration.sql +32 -0
  15. package/prisma/migrations/20251024103700_add_dependency_info/migration.sql +13 -0
  16. package/prisma/migrations/20251025192150_add_dependency_info/migration.sql +19 -0
  17. package/prisma/migrations/20251025192342_add_dependency_info/migration.sql +40 -0
  18. package/prisma/migrations/20251025204613_add_dependency_info/migration.sql +8 -0
  19. package/prisma/migrations/20251026071336_add_dependency_info/migration.sql +25 -0
  20. package/prisma/migrations/20251027062626_add_commit/migration.sql +10 -0
  21. package/prisma/migrations/20251027062748_add_commit/migration.sql +23 -0
  22. package/prisma/migrations/20251027092741_add_commit/migration.sql +17 -0
  23. package/prisma/migrations/20251027112736_add_health_status/migration.sql +16 -0
  24. package/prisma/migrations/20251027140546_init_packages/migration.sql +16 -0
  25. package/prisma/migrations/20251029073436_added_package_heath_key/migration.sql +34 -0
  26. package/prisma/migrations/20251029073830_added_package_health_key/migration.sql +49 -0
  27. package/prisma/migrations/migration_lock.toml +3 -0
  28. package/prisma/schema.prisma +130 -0
  29. package/src/cli.ts +68 -0
  30. package/src/gitService.ts +274 -0
  31. package/src/index.ts +1366 -0
  32. package/src/utils/helpers.js +199 -0
  33. package/src/utils/helpers.js.map +1 -0
  34. package/src/utils/helpers.ts +223 -0
  35. package/tsconfig.json +15 -0
  36. package/tsconfig.o.json +29 -0
package/src/index.ts ADDED
@@ -0,0 +1,1366 @@
1
+ import express, { type Request, type Response, type NextFunction } from 'express';
2
+ import cors from 'cors';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ // import { glob } from 'glob';
6
+ import { json } from 'body-parser';
7
+ import {
8
+ scanner,
9
+ quickScan,
10
+ generateReports,
11
+ funCheckBuildStatus,
12
+ funCheckTestCoverage,
13
+ funCheckLintStatus,
14
+ funCheckSecurityAudit,
15
+ } from '@monodog/monorepo-scanner';
16
+ import { ciStatusManager, getMonorepoCIStatus } from '@monodog/ci-status';
17
+ import {
18
+ scanMonorepo,
19
+ generateMonorepoStats,
20
+ findCircularDependencies,
21
+ generateDependencyGraph,
22
+ calculatePackageHealth,
23
+ } from '@monodog/utils/helpers';
24
+ import {storePackage} from './utils/helpers';
25
+ import { PrismaClient } from '@prisma/client';
26
+ // Import the validateConfig function from your utils
27
+ // import { validateConfig } from '../../apps/dashboard/src/components/modules/config-inspector/utils/config.utils';
28
+ import { GitService } from './gitService';
29
+
30
+ export interface HealthMetric {
31
+ name: string;
32
+ value: number;
33
+ status: 'healthy' | 'warning' | 'error';
34
+ description: string;
35
+ }
36
+ export interface HealthMetric {
37
+ name: string;
38
+ value: number;
39
+ status: 'healthy' | 'warning' | 'error';
40
+ description: string;
41
+ }
42
+
43
+ const prisma = new PrismaClient();
44
+ const DEFAULT_PORT = 4000;
45
+ // The main function exported and called by the CLI
46
+ export function startServer(rootPath: string) {
47
+ const app = express();
48
+ const port = process.env.PORT ? parseInt(process.env.PORT) : DEFAULT_PORT;
49
+
50
+ // --- Middleware ---
51
+
52
+ // 1. Logging Middleware
53
+ app.use((_req: Request, _res: Response, next: NextFunction) => {
54
+ console.log(`[SERVER] ${_req.method} ${_req.url} (Root: ${rootPath})`);
55
+ next();
56
+ });
57
+ app.use(cors());
58
+ app.use(json());
59
+ // // 2. CORS (Critical for the frontend app to talk to this local server)
60
+ // // In a production setup, this would be highly restricted, but for local monorepo tools,
61
+ // // we often allow all origins or restrict to a known local hostname/port.
62
+ // app.use((_req: Request, res: Response, next: NextFunction) => {
63
+ // res.setHeader('Access-Control-Allow-Origin', '*'); // Adjust this in production
64
+ // res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
65
+ // res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
66
+ // next();
67
+ // });
68
+
69
+ // // 3. JSON body parser
70
+ // app.use(express.json());
71
+
72
+ // Health check
73
+ app.get('/api/health', (_, res) => {
74
+ res.json({
75
+ status: 'ok',
76
+ timestamp: Date.now(),
77
+ version: '1.0.0',
78
+ services: {
79
+ scanner: 'active',
80
+ ci: 'active',
81
+ database: 'active',
82
+ },
83
+ });
84
+ });
85
+
86
+ // Get all packages from database (DONE)
87
+ app.get('/api/packages', async (_req, res) => {
88
+ try {
89
+ // Try to get packages from database first
90
+ let dbPackages = await prisma.package.findMany();
91
+ if (!dbPackages.length) {
92
+ try {
93
+ const rootDir = path.resolve(rootPath);
94
+ const packages = scanMonorepo(rootDir);
95
+ console.log('packages --> scan', packages.length);
96
+ for (const pkg of packages) {
97
+ await storePackage(pkg)
98
+ }
99
+ } catch (error) {
100
+ throw new Error('Error ' + error);
101
+ }
102
+ dbPackages = await prisma.package.findMany();
103
+ }
104
+ const transformedPackages = dbPackages.map(pkg => {
105
+ // We create a new object 'transformedPkg' based on the database record 'pkg'
106
+ const transformedPkg = { ...pkg };
107
+
108
+ // 1. Maintainers (Your Logic)
109
+ transformedPkg.maintainers = pkg.maintainers
110
+ ? JSON.parse(pkg.maintainers)
111
+ : [];
112
+
113
+ // 2. Tags
114
+ // transformedPkg.tags = pkg.tags
115
+ // ? JSON.parse(pkg.tags)
116
+ // : [];
117
+
118
+ // 3. Scripts/repository (should default to an object, not an array)
119
+ transformedPkg.scripts = pkg.scripts ? JSON.parse(pkg.scripts) : {};
120
+ transformedPkg.repository = pkg.repository
121
+ ? JSON.parse(pkg.repository)
122
+ : {};
123
+
124
+ // 4. Dependencies List
125
+ transformedPkg.dependencies = pkg.dependencies
126
+ ? JSON.parse(pkg.dependencies)
127
+ : [];
128
+ transformedPkg.devDependencies = pkg.devDependencies
129
+ ? JSON.parse(pkg.devDependencies)
130
+ : [];
131
+ transformedPkg.peerDependencies = pkg.peerDependencies
132
+ ? JSON.parse(pkg.peerDependencies)
133
+ : [];
134
+ return transformedPkg; // Return the fully transformed object
135
+ });
136
+ res.json(transformedPackages);
137
+ } catch (error) {
138
+ res.status(500).json({ error: 'Failed to fetch packages, '+error });
139
+ }
140
+ });
141
+
142
+ app.get('/api/packages/refresh', async (_req, res) => {
143
+ try {
144
+ const rootDir = path.resolve(rootPath);
145
+ const packages = scanMonorepo(rootDir);
146
+ console.log('packages -->', packages.length);
147
+ for (const pkg of packages) {
148
+ storePackage(pkg)
149
+ }
150
+
151
+ res.json(packages);
152
+ } catch (error) {
153
+ res.status(500).json({ error: 'Failed to refresh packages' });
154
+ }
155
+ });
156
+
157
+ // Get package details
158
+ app.get('/api/packages/:name', async (_req, res) => {
159
+ try {
160
+ const { name } = _req.params;
161
+ const pkg = await prisma.package.findUnique({
162
+ where: {
163
+ name: name,
164
+ },
165
+ include: {
166
+ dependenciesInfo: true,
167
+ commits: true,
168
+ packageHealth: true,
169
+ },
170
+ });
171
+ if (!pkg) {
172
+ return res.status(404).json({ error: 'Package not found' });
173
+ }
174
+ const transformedPkg = { ...pkg };
175
+
176
+ // --- APPLY PARSING TO EACH FIELD ---
177
+
178
+ // 1. Maintainers (Your Logic)
179
+ // transformedPkg.maintainers = pkg.maintainers
180
+ // ? JSON.parse(pkg.maintainers)
181
+ // : [];
182
+
183
+ // 2. Tags
184
+ // transformedPkg.tags = pkg.tags
185
+ // ? JSON.parse(pkg.tags)
186
+ // : [];
187
+
188
+ // 3. Scripts/repository (should default to an object, not an array)
189
+ transformedPkg.scripts = pkg.scripts ? JSON.parse(pkg.scripts) : {};
190
+ transformedPkg.repository = pkg.repository
191
+ ? JSON.parse(pkg.repository)
192
+ : {};
193
+
194
+ // 4. Dependencies List
195
+ transformedPkg.dependencies = pkg.dependencies
196
+ ? JSON.parse(pkg.dependencies)
197
+ : [];
198
+ transformedPkg.devDependencies = pkg.devDependencies
199
+ ? JSON.parse(pkg.devDependencies)
200
+ : [];
201
+ transformedPkg.peerDependencies = pkg.peerDependencies
202
+ ? JSON.parse(pkg.peerDependencies)
203
+ : [];
204
+ // Get additional package information
205
+ const reports = await generateReports();
206
+ const packageReport = reports.find(r => r.package.name === name);
207
+
208
+ const result = {
209
+ ...transformedPkg,
210
+ report: packageReport,
211
+ ciStatus: await ciStatusManager.getPackageStatus(name),
212
+ };
213
+
214
+ res.json(result);
215
+ } catch (error) {
216
+ res.status(500).json({ error: 'Failed to fetch package details' });
217
+ }
218
+ });
219
+
220
+ // Get commit details
221
+ app.get('/api/commits/:packagePath', async (_req, res) => {
222
+ try {
223
+ const { packagePath } = _req.params;
224
+
225
+ // Decode the package path
226
+ const decodedPath = decodeURIComponent(packagePath);
227
+
228
+ console.log('🔍 Fetching commits for path:', decodedPath);
229
+ console.log('📁 Current working directory:', process.cwd());
230
+
231
+ const gitService = new GitService();
232
+
233
+ // Check if this is an absolute path and convert to relative if needed
234
+ let relativePath = decodedPath;
235
+ const projectRoot = process.cwd();
236
+
237
+ // If it's an absolute path, make it relative to project root
238
+ if (path.isAbsolute(decodedPath)) {
239
+ relativePath = path.relative(projectRoot, decodedPath);
240
+ console.log('🔄 Converted absolute path to relative:', relativePath);
241
+ }
242
+
243
+ // Check if the path exists
244
+ try {
245
+ await fs.promises.access(relativePath);
246
+ console.log('✅ Path exists:', relativePath);
247
+ } catch (fsError) {
248
+ console.log('❌ Path does not exist:', relativePath);
249
+ // Try the original path as well
250
+ try {
251
+ await fs.promises.access(decodedPath);
252
+ console.log('✅ Original path exists:', decodedPath);
253
+ relativePath = decodedPath; // Use original path if it exists
254
+ } catch (secondError) {
255
+ return res.status(404).json({
256
+ error: `Package path not found: ${relativePath} (also tried: ${decodedPath})`,
257
+ });
258
+ }
259
+ }
260
+
261
+ const commits = await gitService.getAllCommits(relativePath);
262
+
263
+ console.log(
264
+ `✅ Successfully fetched ${commits.length} commits for ${relativePath}`
265
+ );
266
+ res.json(commits);
267
+ } catch (error: any) {
268
+ console.error('💥 Error fetching commit details:', error);
269
+ res.status(500).json({
270
+ error: 'Failed to fetch commit details',
271
+ message: error.message,
272
+ stack: process.env.NODE_ENV === 'development' ? error.stack : undefined,
273
+ });
274
+ }
275
+ });
276
+
277
+ // Get dependency graph
278
+ app.get('/api/graph', async (_req, res) => {
279
+ try {
280
+ const packages = scanMonorepo(process.cwd());
281
+ const graph = generateDependencyGraph(packages);
282
+ const circularDeps = findCircularDependencies(packages);
283
+
284
+ res.json({
285
+ ...graph,
286
+ circularDependencies: circularDeps,
287
+ metadata: {
288
+ totalNodes: graph.nodes.length,
289
+ totalEdges: graph.edges.length,
290
+ circularDependencies: circularDeps.length,
291
+ },
292
+ });
293
+ } catch (error) {
294
+ res.status(500).json({ error: 'Failed to generate dependency graph' });
295
+ }
296
+ });
297
+
298
+ // Get monorepo statistics
299
+ app.get('/api/stats', async (_req, res) => {
300
+ try {
301
+ const packages = scanMonorepo(process.cwd());
302
+ const stats = generateMonorepoStats(packages);
303
+
304
+ res.json({
305
+ ...stats,
306
+ timestamp: Date.now(),
307
+ scanDuration: 0, // Would be calculated from actual scan
308
+ });
309
+ } catch (error) {
310
+ res.status(500).json({ error: 'Failed to fetch statistics' });
311
+ }
312
+ });
313
+
314
+ // Get CI status for all packages
315
+ app.get('/api/ci/status', async (_req, res) => {
316
+ try {
317
+ const packages = scanMonorepo(process.cwd());
318
+ const ciStatus = await getMonorepoCIStatus(packages);
319
+ res.json(ciStatus);
320
+ } catch (error) {
321
+ res.status(500).json({ error: 'Failed to fetch CI status' });
322
+ }
323
+ });
324
+
325
+ // Get CI status for specific package
326
+ app.get('/api/ci/packages/:name', async (_req, res) => {
327
+ try {
328
+ const { name } = _req.params;
329
+ const status = await ciStatusManager.getPackageStatus(name);
330
+
331
+ if (!status) {
332
+ return res.status(404).json({ error: 'Package CI status not found' });
333
+ }
334
+
335
+ res.json(status);
336
+ } catch (error) {
337
+ res.status(500).json({ error: 'Failed to fetch package CI status' });
338
+ }
339
+ });
340
+
341
+ // Trigger CI build for package
342
+ app.post('/api/ci/trigger', async (_req, res) => {
343
+ try {
344
+ const { packageName, providerName, branch } = _req.body;
345
+
346
+ if (!packageName) {
347
+ return res.status(400).json({ error: 'Package name is required' });
348
+ }
349
+
350
+ const result = await ciStatusManager.triggerBuild(
351
+ packageName,
352
+ providerName || 'github',
353
+ branch || 'main'
354
+ );
355
+
356
+ if (result.success) {
357
+ res.json({
358
+ success: true,
359
+ buildId: result.buildId,
360
+ message: `Build triggered for ${packageName}`,
361
+ });
362
+ } else {
363
+ res.status(400).json({
364
+ success: false,
365
+ error: result.error,
366
+ });
367
+ }
368
+ } catch (error) {
369
+ res.status(500).json({ error: 'Failed to trigger build' });
370
+ }
371
+ });
372
+
373
+ // Get build logs
374
+ app.get('/api/ci/builds/:buildId/logs', async (_req, res) => {
375
+ try {
376
+ const { buildId } = _req.params;
377
+ const { provider } = _req.query;
378
+
379
+ if (!provider) {
380
+ return res.status(400).json({ error: 'Provider is required' });
381
+ }
382
+
383
+ const logs = await ciStatusManager.getBuildLogs(
384
+ buildId,
385
+ provider as string
386
+ );
387
+ res.json({ buildId, logs });
388
+ } catch (error) {
389
+ res.status(500).json({ error: 'Failed to fetch build logs' });
390
+ }
391
+ });
392
+
393
+ // Get build artifacts
394
+ app.get('/api/ci/builds/:buildId/artifacts', async (_req, res) => {
395
+ try {
396
+ const { buildId } = _req.params;
397
+ const { provider } = _req.query;
398
+
399
+ if (!provider) {
400
+ return res.status(400).json({ error: 'Provider is required' });
401
+ }
402
+
403
+ const artifacts = await ciStatusManager.getBuildArtifacts(
404
+ buildId,
405
+ provider as string
406
+ );
407
+ res.json({ buildId, artifacts });
408
+ } catch (error) {
409
+ res.status(500).json({ error: 'Failed to fetch build artifacts' });
410
+ }
411
+ });
412
+
413
+ // Perform full monorepo scan
414
+ app.post('/api/scan', async (_req, res) => {
415
+ try {
416
+ const { force } = _req.body;
417
+
418
+ if (force) {
419
+ scanner.clearCache();
420
+ }
421
+
422
+ const result = await quickScan();
423
+ res.json({
424
+ success: true,
425
+ message: 'Scan completed successfully',
426
+ result,
427
+ });
428
+ } catch (error) {
429
+ res.status(500).json({
430
+ success: false,
431
+ error: 'Failed to perform scan',
432
+ });
433
+ }
434
+ });
435
+
436
+ // Get scan results
437
+ app.get('/api/scan/results', async (_req, res) => {
438
+ try {
439
+ const result = await quickScan();
440
+ res.json(result);
441
+ } catch (error) {
442
+ res.status(500).json({ error: 'Failed to fetch scan results' });
443
+ }
444
+ });
445
+
446
+ // Export scan results
447
+ app.get('/api/scan/export/:format', async (_req, res) => {
448
+ try {
449
+ const { format } = _req.params;
450
+ const { filename } = _req.query;
451
+
452
+ if (!['json', 'csv', 'html'].includes(format)) {
453
+ return res.status(400).json({ error: 'Invalid export format' });
454
+ }
455
+
456
+ const result = await quickScan();
457
+ const exportData = scanner.exportResults(
458
+ result,
459
+ format as 'json' | 'csv' | 'html'
460
+ );
461
+
462
+ if (format === 'json') {
463
+ res.json(result);
464
+ } else {
465
+ const contentType = format === 'csv' ? 'text/csv' : 'text/html';
466
+ const contentDisposition = filename
467
+ ? `attachment; filename="${filename}"`
468
+ : `attachment; filename="monorepo-scan.${format}"`;
469
+
470
+ res.setHeader('Content-Type', contentType);
471
+ res.setHeader('Content-Disposition', contentDisposition);
472
+ res.send(exportData);
473
+ }
474
+ } catch (error) {
475
+ res.status(500).json({ error: 'Failed to export scan results' });
476
+ }
477
+ });
478
+
479
+ // ---------- HEALTH --------------------
480
+ // Get package health metrics
481
+ app.get('/api/health/packages/:name', async (_req, res) => {
482
+ try {
483
+ console.log('_req.params -->', _req.params);
484
+ const { name } = _req.params;
485
+ const packages = scanMonorepo(process.cwd());
486
+ const packageInfo = packages.find(p => p.name === name);
487
+
488
+ if (!packageInfo) {
489
+ return res.status(404).json({ error: 'Package not found' });
490
+ }
491
+
492
+ // Get health metrics (mock data for now)
493
+ const health = {
494
+ buildStatus: 'success',
495
+ testCoverage: Math.floor(Math.random() * 100),
496
+ lintStatus: 'pass',
497
+ securityAudit: 'pass',
498
+ overallScore: Math.floor(Math.random() * 40) + 60,
499
+ lastUpdated: new Date(),
500
+ };
501
+
502
+ res.json({
503
+ packageName: name,
504
+ health,
505
+ size: {
506
+ size: Math.floor(Math.random() * 1024 * 1024), // Random size
507
+ files: Math.floor(Math.random() * 1000),
508
+ },
509
+ });
510
+ } catch (error) {
511
+ res.status(500).json({ error: 'Failed to fetch health metrics' });
512
+ }
513
+ });
514
+
515
+ // Get all package health metrics
516
+ // app.get('/api/health/packages', async (_req, res) => {
517
+ // try {
518
+ // // Try to get health data from database
519
+ // const healthData = await prisma.healthStatus.findMany();
520
+ // console.log('healthData -->', healthData);
521
+ // // Transform the data to match the expected frontend format
522
+ // const transformedHealthData = healthData.map(health => {
523
+ // const transformedHealth = { ...health };
524
+
525
+ // // Parse any JSON fields that were stored as strings
526
+ // if (health.metrics) {
527
+ // transformedHealth.metrics = JSON.parse(health.metrics);
528
+ // } else {
529
+ // transformedHealth.metrics = [];
530
+ // }
531
+
532
+ // if (health.packageHealth) {
533
+ // transformedHealth.packageHealth = JSON.parse(health.packageHealth);
534
+ // } else {
535
+ // transformedHealth.packageHealth = [];
536
+ // }
537
+
538
+ // // Ensure we have all required fields with defaults
539
+ // return {
540
+ // id: transformedHealth.id,
541
+ // overallScore: transformedHealth.overallScore || 0,
542
+ // metrics: transformedHealth.metrics || [],
543
+ // packageHealth: transformedHealth.packageHealth || [],
544
+ // createdAt: transformedHealth.createdAt,
545
+ // updatedAt: transformedHealth.updatedAt,
546
+ // };
547
+ // });
548
+
549
+ // // Return the latest health data (you might want to sort by createdAt desc)
550
+ // const latestHealthData = transformedHealthData.sort(
551
+ // (a, b) => new Date(b.createdAt) - new Date(a.createdAt)
552
+ // )[0] || {
553
+ // overallScore: 0,
554
+ // metrics: [],
555
+ // packageHealth: [],
556
+ // };
557
+
558
+ // res.json(latestHealthData);
559
+ // } catch (error) {
560
+ // console.error('Error fetching health data:', error);
561
+ // res.status(500).json({ error: 'Failed to fetch health data' });
562
+ // }
563
+ // });
564
+
565
+ app.get('/api/health/packages', async (_req, res) => {
566
+ try {
567
+ // Fetch all package health data from database
568
+ const packageHealthData = await prisma.packageHealth.findMany();
569
+ console.log('packageHealthData -->', packageHealthData.length);
570
+
571
+ // Transform the data to match the expected frontend format
572
+ const packages = packageHealthData.map(pkg => {
573
+ const health = {
574
+ buildStatus: pkg.packageBuildStatus,
575
+ testCoverage: pkg.packageTestCoverage,
576
+ lintStatus: pkg.packageLintStatus,
577
+ securityAudit: pkg.packageSecurity,
578
+ overallScore: pkg.packageOverallScore,
579
+ };
580
+
581
+ return {
582
+ packageName: pkg.packageName,
583
+ health: health,
584
+ isHealthy: pkg.packageOverallScore >= 80,
585
+ };
586
+ });
587
+
588
+ // Calculate summary statistics
589
+ const total = packages.length;
590
+ const healthy = packages.filter(pkg => pkg.isHealthy).length;
591
+ const unhealthy = packages.filter(pkg => !pkg.isHealthy).length;
592
+ const averageScore =
593
+ packages.length > 0
594
+ ? packages.reduce((sum, pkg) => sum + pkg.health.overallScore, 0) /
595
+ packages.length
596
+ : 0;
597
+
598
+ const response = {
599
+ packages: packages,
600
+ summary: {
601
+ total: total,
602
+ healthy: healthy,
603
+ unhealthy: unhealthy,
604
+ averageScore: averageScore,
605
+ },
606
+ };
607
+
608
+ console.log('Transformed health data -->', response.summary);
609
+ res.json(response);
610
+ } catch (error) {
611
+ console.error('Error fetching health data from database:', error);
612
+ res
613
+ .status(500)
614
+ .json({ error: 'Failed to fetch health data from database' });
615
+ }
616
+ });
617
+
618
+ app.get('/api/health/refresh', async (_req, res) => {
619
+ try {
620
+ const rootDir = path.resolve(rootPath);
621
+ const packages = scanMonorepo(rootDir);
622
+ console.log('packages -->', packages.length);
623
+ const healthMetrics = await Promise.all(
624
+ packages.map(async pkg => {
625
+ try {
626
+ // Await each health check function since they return promises
627
+ const buildStatus = await funCheckBuildStatus(pkg);
628
+ const testCoverage = await funCheckTestCoverage(pkg);
629
+ const lintStatus = await funCheckLintStatus(pkg);
630
+ const securityAudit = await funCheckSecurityAudit(pkg);
631
+ // Calculate overall health score
632
+ const overallScore = calculatePackageHealth(
633
+ buildStatus,
634
+ testCoverage,
635
+ lintStatus,
636
+ securityAudit
637
+ );
638
+
639
+ const health = {
640
+ buildStatus: buildStatus,
641
+ testCoverage: testCoverage,
642
+ lintStatus: lintStatus,
643
+ securityAudit: securityAudit,
644
+ overallScore: overallScore.overallScore,
645
+ };
646
+ const packageStatus = health.overallScore >= 80 ? 'healthy' : (health.overallScore >= 60 && health.overallScore < 80 ? 'warning' : 'error');
647
+
648
+ console.log(pkg.name, '-->', health, packageStatus);
649
+
650
+ // FIX: Use upsert to handle existing packages and proper Prisma syntax
651
+ await prisma.packageHealth.upsert({
652
+ where: {
653
+ packageName: pkg.name,
654
+ },
655
+ update: {
656
+ packageOverallScore: overallScore.overallScore,
657
+ packageBuildStatus: buildStatus,
658
+ packageTestCoverage: testCoverage,
659
+ packageLintStatus: lintStatus,
660
+ packageSecurity: securityAudit,
661
+ packageDependencies: '',
662
+ updatedAt: new Date(),
663
+ package: {
664
+ update: {
665
+ where: { name: pkg.name },
666
+ data: { status: packageStatus },
667
+ },
668
+ },
669
+ },
670
+ create: {
671
+ packageName: pkg.name,
672
+ packageOverallScore: overallScore.overallScore,
673
+ packageBuildStatus: buildStatus,
674
+ packageTestCoverage: testCoverage,
675
+ packageLintStatus: lintStatus,
676
+ packageSecurity: securityAudit,
677
+ packageDependencies: '',
678
+ },
679
+ });
680
+
681
+ return {
682
+ packageName: pkg.name,
683
+ health,
684
+ isHealthy: health.overallScore >= 80,
685
+ };
686
+ } catch (error) {
687
+ return {
688
+ packageName: pkg.name,
689
+ health: null,
690
+ isHealthy: false,
691
+ error: 'Failed to fetch health metrics',
692
+ };
693
+ }
694
+ })
695
+ );
696
+ res.json({
697
+ packages: healthMetrics,
698
+ summary: {
699
+ total: packages.length,
700
+ healthy: healthMetrics.filter(h => h.isHealthy).length,
701
+ unhealthy: healthMetrics.filter(h => !h.isHealthy).length,
702
+ averageScore:
703
+ healthMetrics
704
+ .filter(h => h.health)
705
+ .reduce((sum, h) => sum + h.health!.overallScore, 0) /
706
+ healthMetrics.filter(h => h.health).length,
707
+ },
708
+ });
709
+ } catch (error) {
710
+ res.status(500).json({ error: 'Failed to fetch health metrics' });
711
+ }
712
+ });
713
+ // function calculateHealthStatus(
714
+ // healthy: number,
715
+ // total: number
716
+ // ): 'healthy' | 'warning' | 'error' {
717
+ // if (total === 0) return 'healthy';
718
+ // const ratio = healthy / total;
719
+ // if (ratio >= 0.8) return 'healthy';
720
+ // if (ratio >= 0.6) return 'warning';
721
+ // return 'error';
722
+ // }
723
+ // Search packages
724
+ app.get('/api/search', async (_req, res) => {
725
+ try {
726
+ const { q: query, type, status } = _req.query;
727
+ const packages = scanMonorepo(process.cwd());
728
+
729
+ let filtered = packages;
730
+
731
+ // Filter by search query
732
+ if (query) {
733
+ const searchTerm = (query as string).toLowerCase();
734
+ filtered = filtered.filter(
735
+ pkg =>
736
+ pkg.name.toLowerCase().includes(searchTerm) ||
737
+ pkg.description?.toLowerCase().includes(searchTerm)
738
+ );
739
+ }
740
+
741
+ // Filter by type
742
+ if (type && type !== 'all') {
743
+ filtered = filtered.filter(pkg => pkg.type === type);
744
+ }
745
+
746
+ // Filter by status (would need health data)
747
+ if (status && status !== 'all') {
748
+ // This would filter by actual health status
749
+ // For now, just return all packages
750
+ }
751
+
752
+ res.json({
753
+ query,
754
+ results: filtered,
755
+ total: filtered.length,
756
+ filters: { type, status },
757
+ });
758
+ } catch (error) {
759
+ res.status(500).json({ error: 'Failed to search packages' });
760
+ }
761
+ });
762
+
763
+ // Get recent activity
764
+ app.get('/api/activity', async (_req, res) => {
765
+ try {
766
+ const { limit = 20 } = _req.query;
767
+ const packages = scanMonorepo(process.cwd());
768
+
769
+ // Mock recent activity data
770
+ const activities = packages
771
+ .slice(0, Math.min(Number(limit), packages.length))
772
+ .map((pkg, index) => ({
773
+ id: `activity-${Date.now()}-${index}`,
774
+ type: [
775
+ 'package_updated',
776
+ 'build_success',
777
+ 'test_passed',
778
+ 'dependency_updated',
779
+ ][Math.floor(Math.random() * 4)],
780
+ packageName: pkg.name,
781
+ message: `Activity for ${pkg.name}`,
782
+ timestamp: new Date(
783
+ Date.now() - Math.random() * 7 * 24 * 60 * 60 * 1000
784
+ ), // Random time in last week
785
+ metadata: {
786
+ version: pkg.version,
787
+ type: pkg.type,
788
+ },
789
+ }));
790
+
791
+ // Sort by timestamp (newest first)
792
+ activities.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
793
+
794
+ res.json({
795
+ activities: activities.slice(0, Number(limit)),
796
+ total: activities.length,
797
+ });
798
+ } catch (error) {
799
+ res.status(500).json({ error: 'Failed to fetch recent activity' });
800
+ }
801
+ });
802
+
803
+ // Get system information
804
+ app.get('/api/system', (_, res) => {
805
+ res.json({
806
+ nodeVersion: process.version,
807
+ platform: process.platform,
808
+ arch: process.arch,
809
+ memory: process.memoryUsage(),
810
+ uptime: process.uptime(),
811
+ pid: process.pid,
812
+ cwd: process.cwd(),
813
+ env: {
814
+ NODE_ENV: process.env.NODE_ENV,
815
+ PORT: process.env.PORT,
816
+ },
817
+ });
818
+ });
819
+
820
+ // ------------------------- CONFIGURATION TAB ------------------------- //
821
+ // Get all configuration files from the file system
822
+ app.get('/api/config/files', async (_req, res) => {
823
+ try {
824
+ // Find the monorepo root instead of using process.cwd()
825
+ const rootDir = findMonorepoRoot();
826
+ console.log('Monorepo root directory:', rootDir);
827
+ console.log('Backend directory:', __dirname);
828
+
829
+ const configFiles = await scanConfigFiles(rootDir);
830
+
831
+ // Transform to match frontend ConfigFile interface
832
+ const transformedFiles = configFiles.map(file => ({
833
+ id: file.id,
834
+ name: file.name,
835
+ path: file.path,
836
+ type: file.type,
837
+ content: file.content,
838
+ size: file.size,
839
+ lastModified: file.lastModified,
840
+ hasSecrets: file.hasSecrets,
841
+ isEditable: true,
842
+ // validation: validateConfig(file.content, file.name),
843
+ }));
844
+
845
+ res.json({
846
+ success: true,
847
+ files: transformedFiles,
848
+ total: transformedFiles.length,
849
+ timestamp: Date.now(),
850
+ });
851
+ } catch (error) {
852
+ console.error('Error fetching configuration files:', error);
853
+ res.status(500).json({
854
+ success: false,
855
+ error: 'Failed to fetch configuration files',
856
+ });
857
+ }
858
+ });
859
+
860
+ /**
861
+ * Find the monorepo root by looking for package.json with workspaces or pnpm-workspace.yaml
862
+ */
863
+ function findMonorepoRoot(): string {
864
+ let currentDir = __dirname;
865
+
866
+ while (currentDir !== path.parse(currentDir).root) {
867
+ const packageJsonPath = path.join(currentDir, 'package.json');
868
+ const pnpmWorkspacePath = path.join(currentDir, 'pnpm-workspace.yaml');
869
+
870
+ // Check if this directory has package.json with workspaces or pnpm-workspace.yaml
871
+ if (fs.existsSync(packageJsonPath)) {
872
+ try {
873
+ const packageJson = JSON.parse(
874
+ fs.readFileSync(packageJsonPath, 'utf8')
875
+ );
876
+ // If it has workspaces or is the root monorepo package
877
+ if (packageJson.workspaces || fs.existsSync(pnpmWorkspacePath)) {
878
+ console.log('✅ Found monorepo root:', currentDir);
879
+ return currentDir;
880
+ }
881
+ } catch (error) {
882
+ // Continue searching if package.json is invalid
883
+ }
884
+ }
885
+
886
+ // Check if we're at the git root
887
+ const gitPath = path.join(currentDir, '.git');
888
+ if (fs.existsSync(gitPath)) {
889
+ console.log('✅ Found git root (likely monorepo root):', currentDir);
890
+ return currentDir;
891
+ }
892
+
893
+ // Go up one directory
894
+ const parentDir = path.dirname(currentDir);
895
+ if (parentDir === currentDir) break; // Prevent infinite loop
896
+ currentDir = parentDir;
897
+ }
898
+
899
+ // Fallback to process.cwd() if we can't find the root
900
+ console.log(
901
+ '⚠️ Could not find monorepo root, using process.cwd():',
902
+ process.cwd()
903
+ );
904
+ return process.cwd();
905
+ }
906
+
907
+ // Helper function to scan for configuration files
908
+ async function scanConfigFiles(rootDir: string): Promise<any[]> {
909
+ const configPatterns = [
910
+ // Root level config files
911
+ 'package.json',
912
+ 'pnpm-workspace.yaml',
913
+ 'pnpm-lock.yaml',
914
+ 'turbo.json',
915
+ 'tsconfig.json',
916
+ '.eslintrc.*',
917
+ '.prettierrc',
918
+ '.prettierignore',
919
+ '.editorconfig',
920
+ '.nvmrc',
921
+ '.gitignore',
922
+ 'commitlint.config.js',
923
+ '.releaserc.json',
924
+ 'env.example',
925
+
926
+ // App-specific config files
927
+ 'vite.config.*',
928
+ 'tailwind.config.*',
929
+ 'postcss.config.*',
930
+ 'components.json',
931
+
932
+ // Package-specific config files
933
+ 'jest.config.*',
934
+ '.env*',
935
+ 'dockerfile*',
936
+ ];
937
+
938
+ const configFiles: any[] = [];
939
+ const scannedPaths = new Set();
940
+
941
+ function scanDirectory(dir: string, depth = 0) {
942
+ if (scannedPaths.has(dir) || depth > 8) return; // Limit depth for safety
943
+ scannedPaths.add(dir);
944
+
945
+ try {
946
+ const items = fs.readdirSync(dir, { withFileTypes: true });
947
+
948
+ for (const item of items) {
949
+ const fullPath = path.join(dir, item.name);
950
+
951
+ if (item.isDirectory()) {
952
+ // Skip node_modules and other non-source directories
953
+ if (!shouldSkipDirectory(item.name, depth)) {
954
+ scanDirectory(fullPath, depth + 1);
955
+ }
956
+ } else {
957
+ // Check if file matches config patterns
958
+ if (isConfigFile(item.name)) {
959
+ try {
960
+ const stats = fs.statSync(fullPath);
961
+ const content = fs.readFileSync(fullPath, 'utf8');
962
+ const relativePath =
963
+ fullPath.replace(rootDir, '').replace(/\\/g, '/') ||
964
+ `/${item.name}`;
965
+
966
+ configFiles.push({
967
+ id: relativePath,
968
+ name: item.name,
969
+ path: relativePath,
970
+ type: getFileType(item.name),
971
+ content: content,
972
+ size: stats.size,
973
+ lastModified: stats.mtime.toISOString(),
974
+ hasSecrets: containsSecrets(content, item.name),
975
+ });
976
+ } catch (error) {
977
+ console.warn(`Could not read file: ${fullPath}`);
978
+ }
979
+ }
980
+ }
981
+ }
982
+ } catch (error) {
983
+ console.warn(`Could not scan directory: ${dir}`);
984
+ }
985
+ }
986
+
987
+ function shouldSkipDirectory(dirName: string, depth: number): boolean {
988
+ const skipDirs = [
989
+ 'node_modules',
990
+ 'dist',
991
+ 'build',
992
+ '.git',
993
+ '.next',
994
+ '.vscode',
995
+ '.turbo',
996
+ '.husky',
997
+ '.github',
998
+ '.vite',
999
+ 'migrations',
1000
+ 'coverage',
1001
+ '.cache',
1002
+ 'tmp',
1003
+ 'temp',
1004
+ ];
1005
+
1006
+ // At root level, be more permissive
1007
+ if (depth === 0) {
1008
+ return skipDirs.includes(dirName);
1009
+ }
1010
+
1011
+ // Deeper levels, skip more directories
1012
+ return skipDirs.includes(dirName) || dirName.startsWith('.');
1013
+ }
1014
+
1015
+ function isConfigFile(filename: string): boolean {
1016
+ return configPatterns.some(pattern => {
1017
+ if (pattern.includes('*')) {
1018
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
1019
+ return regex.test(filename.toLowerCase());
1020
+ }
1021
+ return filename.toLowerCase() === pattern.toLowerCase();
1022
+ });
1023
+ }
1024
+
1025
+ console.log(`🔍 Scanning for config files in: ${rootDir}`);
1026
+
1027
+ // Start scanning from root
1028
+ scanDirectory(rootDir);
1029
+
1030
+ // Sort files by path for consistent ordering
1031
+ configFiles.sort((a, b) => a.path.localeCompare(b.path));
1032
+
1033
+ console.log(`📁 Found ${configFiles.length} configuration files`);
1034
+
1035
+ // Log some sample files for debugging
1036
+ if (configFiles.length > 0) {
1037
+ console.log('Sample config files found:');
1038
+ configFiles.slice(0, 5).forEach(file => {
1039
+ console.log(` - ${file.path} (${file.type})`);
1040
+ });
1041
+ if (configFiles.length > 5) {
1042
+ console.log(` ... and ${configFiles.length - 5} more`);
1043
+ }
1044
+ }
1045
+
1046
+ return configFiles;
1047
+ }
1048
+
1049
+ function getFileType(filename: string): string {
1050
+ const extension = filename.split('.').pop()?.toLowerCase();
1051
+ switch (extension) {
1052
+ case 'json':
1053
+ return 'json';
1054
+ case 'yaml':
1055
+ case 'yml':
1056
+ return 'yaml';
1057
+ case 'js':
1058
+ return 'javascript';
1059
+ case 'ts':
1060
+ return 'typescript';
1061
+ case 'env':
1062
+ return 'env';
1063
+ case 'md':
1064
+ return 'markdown';
1065
+ case 'cjs':
1066
+ return 'javascript';
1067
+ case 'config':
1068
+ return 'text';
1069
+ case 'example':
1070
+ return filename.includes('.env') ? 'env' : 'text';
1071
+ default:
1072
+ return 'text';
1073
+ }
1074
+ }
1075
+
1076
+ function containsSecrets(content: string, filename: string): boolean {
1077
+ // Only check .env files and files that might contain secrets
1078
+ if (!filename.includes('.env') && !filename.includes('config')) {
1079
+ return false;
1080
+ }
1081
+
1082
+ const secretPatterns = [
1083
+ /password\s*=\s*[^\s]/i,
1084
+ /secret\s*=\s*[^\s]/i,
1085
+ /key\s*=\s*[^\s]/i,
1086
+ /token\s*=\s*[^\s]/i,
1087
+ /auth\s*=\s*[^\s]/i,
1088
+ /credential\s*=\s*[^\s]/i,
1089
+ /api_key\s*=\s*[^\s]/i,
1090
+ /private_key\s*=\s*[^\s]/i,
1091
+ /DATABASE_URL/i,
1092
+ /JWT_SECRET/i,
1093
+ /GITHUB_TOKEN/i,
1094
+ ];
1095
+
1096
+ return secretPatterns.some(pattern => pattern.test(content));
1097
+ }
1098
+
1099
+ // Save configuration file
1100
+ // app.put('/api/config/files/:id', async (_req, res) => {
1101
+ // try {
1102
+ // const { id } = _req.params;
1103
+ // const { content } = _req.body;
1104
+
1105
+ // if (!content) {
1106
+ // return res.status(400).json({
1107
+ // success: false,
1108
+ // error: 'Content is required',
1109
+ // });
1110
+ // }
1111
+
1112
+ // const rootDir = process.cwd();
1113
+ // const filePath = path.join(rootDir, id.startsWith('/') ? id.slice(1) : id);
1114
+
1115
+ // // Security check: ensure the file is within the project directory
1116
+ // if (!filePath.startsWith(rootDir)) {
1117
+ // return res.status(403).json({
1118
+ // success: false,
1119
+ // error: 'Access denied',
1120
+ // });
1121
+ // }
1122
+
1123
+ // // Check if file exists and is writable
1124
+ // try {
1125
+ // await fs.promises.access(filePath, fs.constants.W_OK);
1126
+ // } catch (error) {
1127
+ // return res.status(403).json({
1128
+ // success: false,
1129
+ // error: 'File is not writable or does not exist',
1130
+ // });
1131
+ // }
1132
+
1133
+ // // // Backup the original file (optional but recommended)
1134
+ // // const backupPath = `${filePath}.backup-${Date.now()}`;
1135
+ // // try {
1136
+ // // await fs.promises.copyFile(filePath, backupPath);
1137
+ // // } catch (error) {
1138
+ // // console.warn('Could not create backup file:', error);
1139
+ // // }
1140
+
1141
+ // // Write the new content
1142
+ // await fs.promises.writeFile(filePath, content, 'utf8');
1143
+
1144
+ // // Get file stats for updated information
1145
+ // const stats = await fs.promises.stat(filePath);
1146
+ // const filename = path.basename(filePath);
1147
+
1148
+ // const updatedFile = {
1149
+ // id: id,
1150
+ // name: filename,
1151
+ // path: id,
1152
+ // type: getFileType(filename),
1153
+ // content: content,
1154
+ // size: stats.size,
1155
+ // lastModified: stats.mtime.toISOString(),
1156
+ // hasSecrets: containsSecrets(content, filename),
1157
+ // isEditable: true,
1158
+ // validation: validateConfig(content, filename),
1159
+ // };
1160
+
1161
+ // res.json({
1162
+ // success: true,
1163
+ // file: updatedFile,
1164
+ // message: 'File saved successfully',
1165
+ // // backupCreated: fs.existsSync(backupPath),
1166
+ // });
1167
+ // } catch (error) {
1168
+ // console.error('Error saving configuration file:', error);
1169
+ // res.status(500).json({
1170
+ // success: false,
1171
+ // error: 'Failed to save configuration file',
1172
+ // });
1173
+ // }
1174
+ // });
1175
+
1176
+ app.put('/api/config/files/:id', async (_req, res) => {
1177
+ try {
1178
+ const { id } = _req.params;
1179
+ const { content } = _req.body;
1180
+
1181
+ if (!content) {
1182
+ return res.status(400).json({
1183
+ success: false,
1184
+ error: 'Content is required',
1185
+ });
1186
+ }
1187
+
1188
+ // Use the monorepo root
1189
+ const rootDir = findMonorepoRoot();
1190
+ const filePath = path.join(rootDir, id.startsWith('/') ? id.slice(1) : id);
1191
+
1192
+ console.log('💾 Saving file:', filePath);
1193
+ console.log('📁 Root directory:', rootDir);
1194
+
1195
+ // Security check: ensure the file is within the project directory
1196
+ if (!filePath.startsWith(rootDir)) {
1197
+ return res.status(403).json({
1198
+ success: false,
1199
+ error: 'Access denied',
1200
+ });
1201
+ }
1202
+
1203
+ // Check if file exists and is writable
1204
+ try {
1205
+ await fs.promises.access(filePath, fs.constants.W_OK);
1206
+ } catch (error) {
1207
+ return res.status(403).json({
1208
+ success: false,
1209
+ error: 'File is not writable or does not exist',
1210
+ });
1211
+ }
1212
+
1213
+ // Write the new content
1214
+ await fs.promises.writeFile(filePath, content, 'utf8');
1215
+
1216
+ // Get file stats for updated information
1217
+ const stats = await fs.promises.stat(filePath);
1218
+ const filename = path.basename(filePath);
1219
+
1220
+ const updatedFile = {
1221
+ id: id,
1222
+ name: filename,
1223
+ path: id,
1224
+ type: getFileType(filename),
1225
+ content: content,
1226
+ size: stats.size,
1227
+ lastModified: stats.mtime.toISOString(),
1228
+ hasSecrets: containsSecrets(content, filename),
1229
+ isEditable: true,
1230
+ // validation: validateConfig(content, filename),
1231
+ };
1232
+
1233
+ res.json({
1234
+ success: true,
1235
+ file: updatedFile,
1236
+ message: 'File saved successfully',
1237
+ });
1238
+ } catch (error) {
1239
+ console.error('Error saving configuration file:', error);
1240
+ res.status(500).json({
1241
+ success: false,
1242
+ error: 'Failed to save configuration file',
1243
+ });
1244
+ }
1245
+ });
1246
+
1247
+ // Error handling middleware
1248
+ app.use(
1249
+ (
1250
+ error: any,
1251
+ _req: express.Request,
1252
+ res: express.Response,
1253
+ _next: express.NextFunction
1254
+ ) => {
1255
+ console.error('Error:', error);
1256
+ res.status(500).json({
1257
+ error: 'Internal server error',
1258
+ message: error.message,
1259
+ timestamp: Date.now(),
1260
+ });
1261
+ }
1262
+ );
1263
+
1264
+ // 404 handler
1265
+ app.use('*', (_, res) => {
1266
+ res.status(404).json({
1267
+ error: 'Endpoint not found',
1268
+ timestamp: Date.now(),
1269
+ });
1270
+ });
1271
+
1272
+ const PORT = process.env.PORT || 4000;
1273
+
1274
+ app.listen(PORT, () => {
1275
+ console.log(`🚀 Backend server running on http://localhost:${PORT}`);
1276
+ console.log(`📊 API endpoints available:`);
1277
+ console.log(` - GET /api/health`);
1278
+ console.log(` - GET /api/packages/refresh`);
1279
+ console.log(` - GET /api/packages`);
1280
+ console.log(` - GET /api/packages/:name`);
1281
+ console.log(` - GET /api/commits/:packagePath`);
1282
+ console.log(` - GET /api/graph`);
1283
+ console.log(` - GET /api/stats`);
1284
+ console.log(` - GET /api/ci/status`);
1285
+ console.log(` - POST /api/ci/trigger`);
1286
+ console.log(` - POST /api/scan`);
1287
+ console.log(` - GET /api/health/packages`);
1288
+ console.log(` - GET /api/search`);
1289
+ console.log(` - GET /api/activity`);
1290
+ console.log(` - GET /api/system`);
1291
+ }).on('error', (err) => {
1292
+ // Handle common errors like EADDRINUSE (port already in use)
1293
+ if (err.message.includes('EADDRINUSE')) {
1294
+ console.error(`Error: Port ${port} is already in use. Please specify a different port via the PORT environment variable.`);
1295
+ process.exit(1);
1296
+ } else {
1297
+ console.error('Server failed to start:', err);
1298
+ process.exit(1);
1299
+ }
1300
+ });
1301
+
1302
+ // export default app;
1303
+
1304
+ // const overallScore =
1305
+ // healthMetrics.reduce((sum, h) => sum + h.health!.overallScore, 0) /
1306
+ // healthMetrics.length;
1307
+
1308
+ // const metrics: HealthMetric[] = [
1309
+ // {
1310
+ // name: 'Package Health',
1311
+ // value: healthMetrics.filter(h => h.isHealthy).length || 0,
1312
+ // status: calculateHealthStatus(
1313
+ // healthMetrics.filter(h => h.isHealthy).length,
1314
+ // packages.length
1315
+ // ),
1316
+ // description: `${healthMetrics.filter(h => h.isHealthy).length || 0} healthy packages out of ${packages.length || 0}`,
1317
+ // },
1318
+ // {
1319
+ // name: 'Overall Score',
1320
+ // value: Math.round(overallScore),
1321
+ // status:
1322
+ // Math.round(overallScore) >= 80
1323
+ // ? 'healthy'
1324
+ // : Math.round(overallScore) >= 60
1325
+ // ? 'warning'
1326
+ // : 'error',
1327
+ // description: `Average health score: ${Math.round(overallScore)}/100`,
1328
+ // },
1329
+ // {
1330
+ // name: 'Unhealthy Packages',
1331
+ // value: healthMetrics.filter(h => !h.isHealthy).length || 0,
1332
+ // status:
1333
+ // (healthMetrics.filter(h => !h.isHealthy).length || 0) === 0
1334
+ // ? 'healthy'
1335
+ // : (healthMetrics.filter(h => !h.isHealthy).length || 0) <= 2
1336
+ // ? 'warning'
1337
+ // : 'error',
1338
+ // description: `${healthMetrics.filter(h => !h.isHealthy).length || 0} packages need attention`,
1339
+ // },
1340
+ // ];
1341
+
1342
+ // const packageHealth = packages.map((pkg: any) => ({
1343
+ // package: pkg.packageName,
1344
+ // score: pkg.health?.overallScore || 0,
1345
+ // issues: pkg.error
1346
+ // ? [pkg.error]
1347
+ // : (pkg.health?.overallScore || 0) < 80
1348
+ // ? ['Needs improvement']
1349
+ // : [],
1350
+ // }));
1351
+
1352
+ // res.status(200).json({
1353
+ // overallScore,
1354
+ // metrics,
1355
+ // packageHealth,
1356
+ // });
1357
+
1358
+ }
1359
+
1360
+
1361
+ // This is a test file to ensure the TypeScript compiler can find an input.
1362
+ // export const runBackend = (name: string): string => {
1363
+ // return `Backend running successfully for ${name}.`;
1364
+ // };
1365
+
1366
+ // console.log(runBackend("MonoDog"));