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