@manojkmfsi/monodog 1.1.6 → 1.1.8
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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/config/swagger-config.ts +0 -344
- package/src/config-loader.ts +0 -97
- package/src/constants/index.ts +0 -13
- package/src/constants/middleware.ts +0 -83
- package/src/constants/port.ts +0 -20
- package/src/constants/security.ts +0 -78
- package/src/controllers/commit-controller.ts +0 -31
- package/src/controllers/config-controller.ts +0 -42
- package/src/controllers/health-controller.ts +0 -24
- package/src/controllers/package-controller.ts +0 -67
- package/src/get-db-url.ts +0 -9
- package/src/index.ts +0 -9
- package/src/middleware/dashboard-startup.ts +0 -161
- package/src/middleware/error-handler.ts +0 -50
- package/src/middleware/index.ts +0 -20
- package/src/middleware/logger.ts +0 -65
- package/src/middleware/security.ts +0 -90
- package/src/middleware/server-startup.ts +0 -153
- package/src/middleware/swagger-middleware.ts +0 -58
- package/src/repositories/commit-repository.ts +0 -108
- package/src/repositories/dependency-repository.ts +0 -110
- package/src/repositories/index.ts +0 -10
- package/src/repositories/package-health-repository.ts +0 -75
- package/src/repositories/package-repository.ts +0 -142
- package/src/repositories/prisma-client.ts +0 -25
- package/src/routes/commit-routes.ts +0 -10
- package/src/routes/config-routes.ts +0 -14
- package/src/routes/health-routes.ts +0 -14
- package/src/routes/package-routes.ts +0 -22
- package/src/serve.ts +0 -41
- package/src/services/commit-service.ts +0 -44
- package/src/services/config-service.ts +0 -391
- package/src/services/git-service.ts +0 -140
- package/src/services/health-service.ts +0 -162
- package/src/services/package-service.ts +0 -228
- package/src/setup.ts +0 -78
- package/src/types/ci.ts +0 -122
- package/src/types/config.ts +0 -22
- package/src/types/database.ts +0 -67
- package/src/types/git.ts +0 -33
- package/src/types/health.ts +0 -11
- package/src/types/index.ts +0 -23
- package/src/types/package.ts +0 -39
- package/src/types/scanner.ts +0 -31
- package/src/types/swagger-jsdoc.d.ts +0 -15
- package/src/utils/ci-status.ts +0 -535
- package/src/utils/db-utils.ts +0 -92
- package/src/utils/health-utils.ts +0 -67
- package/src/utils/monorepo-scanner.ts +0 -576
- package/src/utils/utilities.ts +0 -427
|
@@ -1,391 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
import { AppLogger } from '../middleware/logger';
|
|
5
|
-
import { PackageRepository } from '../repositories';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Configuration file interface
|
|
9
|
-
*/
|
|
10
|
-
interface ConfigFile {
|
|
11
|
-
id: string;
|
|
12
|
-
name: string;
|
|
13
|
-
path: string;
|
|
14
|
-
type: string;
|
|
15
|
-
content: string;
|
|
16
|
-
size: number;
|
|
17
|
-
lastModified: string;
|
|
18
|
-
hasSecrets: boolean;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
interface TransformedConfigFile extends ConfigFile {
|
|
22
|
-
isEditable: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Helper function to scan for configuration files
|
|
26
|
-
async function scanConfigFiles(rootDir: string): Promise<any[]> {
|
|
27
|
-
const configPatterns = [
|
|
28
|
-
// Root level config files
|
|
29
|
-
'package.json',
|
|
30
|
-
'pnpm-workspace.yaml',
|
|
31
|
-
'pnpm-lock.yaml',
|
|
32
|
-
'turbo.json',
|
|
33
|
-
'tsconfig.json',
|
|
34
|
-
'.eslintrc.*',
|
|
35
|
-
'.prettierrc',
|
|
36
|
-
'.prettierignore',
|
|
37
|
-
'.editorconfig',
|
|
38
|
-
'.nvmrc',
|
|
39
|
-
'.gitignore',
|
|
40
|
-
'commitlint.config.js',
|
|
41
|
-
'.releaserc.json',
|
|
42
|
-
'env.example',
|
|
43
|
-
|
|
44
|
-
// App-specific config files
|
|
45
|
-
'vite.config.*',
|
|
46
|
-
'tailwind.config.*',
|
|
47
|
-
'postcss.config.*',
|
|
48
|
-
'components.json',
|
|
49
|
-
|
|
50
|
-
// Package-specific config files
|
|
51
|
-
'jest.config.*',
|
|
52
|
-
'.env*',
|
|
53
|
-
'dockerfile*',
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
const configFiles: ConfigFile[] = [];
|
|
57
|
-
const scannedPaths = new Set();
|
|
58
|
-
|
|
59
|
-
function scanDirectory(dir: string, depth = 0) {
|
|
60
|
-
if (scannedPaths.has(dir) || depth > 8) return; // Limit depth for safety
|
|
61
|
-
scannedPaths.add(dir);
|
|
62
|
-
|
|
63
|
-
try {
|
|
64
|
-
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
65
|
-
|
|
66
|
-
for (const item of items) {
|
|
67
|
-
const fullPath = path.join(dir, item.name);
|
|
68
|
-
|
|
69
|
-
if (item.isDirectory()) {
|
|
70
|
-
// Skip node_modules and other non-source directories
|
|
71
|
-
if (!shouldSkipDirectory(item.name, depth)) {
|
|
72
|
-
scanDirectory(fullPath, depth + 1);
|
|
73
|
-
}
|
|
74
|
-
} else {
|
|
75
|
-
// Check if file matches config patterns
|
|
76
|
-
if (isConfigFile(item.name)) {
|
|
77
|
-
try {
|
|
78
|
-
const stats = fs.statSync(fullPath);
|
|
79
|
-
const content = fs.readFileSync(fullPath, 'utf8');
|
|
80
|
-
const relativePath =
|
|
81
|
-
fullPath.replace(rootDir, '').replace(/\\/g, '/') ||
|
|
82
|
-
`/${item.name}`;
|
|
83
|
-
|
|
84
|
-
configFiles.push({
|
|
85
|
-
id: relativePath,
|
|
86
|
-
name: item.name,
|
|
87
|
-
path: relativePath,
|
|
88
|
-
type: getFileType(item.name),
|
|
89
|
-
content: content,
|
|
90
|
-
size: stats.size,
|
|
91
|
-
lastModified: stats.mtime.toISOString(),
|
|
92
|
-
hasSecrets: containsSecrets(content, item.name),
|
|
93
|
-
});
|
|
94
|
-
} catch (error) {
|
|
95
|
-
AppLogger.warn(`Could not read file: ${fullPath}`);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
} catch (error) {
|
|
101
|
-
AppLogger.warn(`Could not scan directory: ${dir}`);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function shouldSkipDirectory(dirName: string, depth: number): boolean {
|
|
106
|
-
const skipDirs = [
|
|
107
|
-
'node_modules',
|
|
108
|
-
'dist',
|
|
109
|
-
'build',
|
|
110
|
-
'.git',
|
|
111
|
-
'.next',
|
|
112
|
-
'.vscode',
|
|
113
|
-
'.turbo',
|
|
114
|
-
'.husky',
|
|
115
|
-
'.github',
|
|
116
|
-
'.vite',
|
|
117
|
-
'migrations',
|
|
118
|
-
'coverage',
|
|
119
|
-
'.cache',
|
|
120
|
-
'tmp',
|
|
121
|
-
'temp',
|
|
122
|
-
];
|
|
123
|
-
|
|
124
|
-
// At root level, be more permissive
|
|
125
|
-
if (depth === 0) {
|
|
126
|
-
return skipDirs.includes(dirName);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// Deeper levels, skip more directories
|
|
130
|
-
return skipDirs.includes(dirName) || dirName.startsWith('.');
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function isConfigFile(filename: string): boolean {
|
|
134
|
-
return configPatterns.some(pattern => {
|
|
135
|
-
if (pattern.includes('*')) {
|
|
136
|
-
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
137
|
-
return regex.test(filename.toLowerCase());
|
|
138
|
-
}
|
|
139
|
-
return filename.toLowerCase() === pattern.toLowerCase();
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
AppLogger.info(`Scanning for config files in: ${rootDir}`);
|
|
144
|
-
|
|
145
|
-
// Start scanning from root
|
|
146
|
-
scanDirectory(rootDir);
|
|
147
|
-
|
|
148
|
-
// Sort files by path for consistent ordering
|
|
149
|
-
configFiles.sort((a, b) => a.path.localeCompare(b.path));
|
|
150
|
-
|
|
151
|
-
AppLogger.info(`Found ${configFiles.length} configuration files`);
|
|
152
|
-
|
|
153
|
-
// Log some sample files for debugging
|
|
154
|
-
if (configFiles.length > 0) {
|
|
155
|
-
AppLogger.info('Sample config files found');
|
|
156
|
-
configFiles.slice(0, 5).forEach(file => {
|
|
157
|
-
AppLogger.debug(` - ${file.path} (${file.type})`);
|
|
158
|
-
});
|
|
159
|
-
if (configFiles.length > 5) {
|
|
160
|
-
AppLogger.debug(` ... and ${configFiles.length - 5} more`);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return configFiles;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function containsSecrets(content: string, filename: string): boolean {
|
|
168
|
-
// Only check .env files and files that might contain secrets
|
|
169
|
-
if (!filename.includes('.env') && !filename.includes('config')) {
|
|
170
|
-
return false;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const secretPatterns = [
|
|
174
|
-
/password\s*=\s*[^\s]/i,
|
|
175
|
-
/secret\s*=\s*[^\s]/i,
|
|
176
|
-
/key\s*=\s*[^\s]/i,
|
|
177
|
-
/token\s*=\s*[^\s]/i,
|
|
178
|
-
/auth\s*=\s*[^\s]/i,
|
|
179
|
-
/credential\s*=\s*[^\s]/i,
|
|
180
|
-
/api_key\s*=\s*[^\s]/i,
|
|
181
|
-
/private_key\s*=\s*[^\s]/i,
|
|
182
|
-
/DATABASE_URL/i,
|
|
183
|
-
/JWT_SECRET/i,
|
|
184
|
-
/GITHUB_TOKEN/i,
|
|
185
|
-
];
|
|
186
|
-
|
|
187
|
-
return secretPatterns.some(pattern => pattern.test(content));
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function getFileType(filename: string): string {
|
|
191
|
-
const extension = filename.split('.').pop()?.toLowerCase();
|
|
192
|
-
switch (extension) {
|
|
193
|
-
case 'json':
|
|
194
|
-
return 'json';
|
|
195
|
-
case 'yaml':
|
|
196
|
-
case 'yml':
|
|
197
|
-
return 'yaml';
|
|
198
|
-
case 'js':
|
|
199
|
-
return 'javascript';
|
|
200
|
-
case 'ts':
|
|
201
|
-
return 'typescript';
|
|
202
|
-
case 'env':
|
|
203
|
-
return 'env';
|
|
204
|
-
case 'md':
|
|
205
|
-
return 'markdown';
|
|
206
|
-
case 'cjs':
|
|
207
|
-
return 'javascript';
|
|
208
|
-
case 'config':
|
|
209
|
-
return 'text';
|
|
210
|
-
case 'example':
|
|
211
|
-
return filename.includes('.env') ? 'env' : 'text';
|
|
212
|
-
default:
|
|
213
|
-
return 'text';
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export const getConfigurationFilesService = async (rootDir: string) => {
|
|
218
|
-
|
|
219
|
-
const configFiles = await scanConfigFiles(rootDir);
|
|
220
|
-
|
|
221
|
-
// Transform to match frontend ConfigFile interface
|
|
222
|
-
const transformedFiles = configFiles.map(file => ({
|
|
223
|
-
id: file.id,
|
|
224
|
-
name: file.name,
|
|
225
|
-
path: file.path,
|
|
226
|
-
type: file.type,
|
|
227
|
-
content: file.content,
|
|
228
|
-
size: file.size,
|
|
229
|
-
lastModified: file.lastModified,
|
|
230
|
-
hasSecrets: file.hasSecrets,
|
|
231
|
-
isEditable: true,
|
|
232
|
-
}));
|
|
233
|
-
|
|
234
|
-
return {
|
|
235
|
-
success: true,
|
|
236
|
-
files: transformedFiles,
|
|
237
|
-
total: transformedFiles.length,
|
|
238
|
-
timestamp: Date.now(),
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export const updateConfigFileService = async (id: string, rootDir: string, content: string) => {
|
|
243
|
-
const filePath = path.join(
|
|
244
|
-
rootDir,
|
|
245
|
-
id.startsWith('/') ? id.slice(1) : id
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
AppLogger.debug('Saving file: ' + filePath);
|
|
249
|
-
AppLogger.debug('Root directory: ' + rootDir);
|
|
250
|
-
|
|
251
|
-
// Security check: ensure the file is within the project directory
|
|
252
|
-
if (!filePath.startsWith(rootDir)) {
|
|
253
|
-
throw new Error('Invalid file path');
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
// Check if file exists and is writable
|
|
257
|
-
try {
|
|
258
|
-
await fs.promises.access(filePath, fs.constants.W_OK);
|
|
259
|
-
} catch (error) {
|
|
260
|
-
throw new Error('File is not writable or does not exist ' + filePath);
|
|
261
|
-
}
|
|
262
|
-
// Write the new content
|
|
263
|
-
await fs.promises.writeFile(filePath, content, 'utf8');
|
|
264
|
-
|
|
265
|
-
// Get file stats for updated information
|
|
266
|
-
const stats = await fs.promises.stat(filePath);
|
|
267
|
-
const filename = path.basename(filePath);
|
|
268
|
-
|
|
269
|
-
const updatedFile = {
|
|
270
|
-
id: id,
|
|
271
|
-
name: filename,
|
|
272
|
-
path: id,
|
|
273
|
-
type: getFileType(filename),
|
|
274
|
-
content: content,
|
|
275
|
-
size: stats.size,
|
|
276
|
-
lastModified: stats.mtime.toISOString(),
|
|
277
|
-
hasSecrets: containsSecrets(content, filename),
|
|
278
|
-
isEditable: true,
|
|
279
|
-
};
|
|
280
|
-
|
|
281
|
-
return {
|
|
282
|
-
success: true,
|
|
283
|
-
file: updatedFile,
|
|
284
|
-
message: 'File saved successfully',
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
export const updatePackageConfigurationService = async (packagePath: string, packageName: string, config: string) => {
|
|
289
|
-
let newConfig;
|
|
290
|
-
try {
|
|
291
|
-
newConfig = JSON.parse(config);
|
|
292
|
-
} catch (error) {
|
|
293
|
-
AppLogger.error('JSON parsing error', error as Error);
|
|
294
|
-
throw new Error('Invalid JSON format: ' + (error instanceof Error ? error.message : 'Unknown error'));
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const packageJsonPath = path.join(packagePath, 'package.json');
|
|
298
|
-
|
|
299
|
-
// Security check: ensure the path is valid
|
|
300
|
-
if (!fs.existsSync(packagePath)) {
|
|
301
|
-
throw new Error('Package directory not found: ' + packagePath);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Check if package.json exists
|
|
305
|
-
if (!fs.existsSync(packageJsonPath)) {
|
|
306
|
-
throw new Error('package.json not found in directory: ' + packageJsonPath);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
// Read the existing package.json to preserve all fields
|
|
310
|
-
const existingContent = await fs.promises.readFile(packageJsonPath, 'utf8');
|
|
311
|
-
let existingConfig;
|
|
312
|
-
try {
|
|
313
|
-
existingConfig = JSON.parse(existingContent);
|
|
314
|
-
} catch (error) {
|
|
315
|
-
throw new Error('Failed to parse existing package.json: ' + (error instanceof Error ? error.message : 'Invalid JSON'));
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Merge the new configuration with existing configuration
|
|
319
|
-
const mergedConfig = {
|
|
320
|
-
...existingConfig,
|
|
321
|
-
name: newConfig.name || existingConfig.name,
|
|
322
|
-
version: newConfig.version || existingConfig.version,
|
|
323
|
-
description:
|
|
324
|
-
newConfig.description !== undefined
|
|
325
|
-
? newConfig.description
|
|
326
|
-
: existingConfig.description,
|
|
327
|
-
license:
|
|
328
|
-
newConfig.license !== undefined
|
|
329
|
-
? newConfig.license
|
|
330
|
-
: existingConfig.license,
|
|
331
|
-
repository: newConfig.repository || existingConfig.repository,
|
|
332
|
-
scripts: newConfig.scripts || existingConfig.scripts,
|
|
333
|
-
dependencies: newConfig.dependencies || existingConfig.dependencies,
|
|
334
|
-
devDependencies:
|
|
335
|
-
newConfig.devDependencies || existingConfig.devDependencies,
|
|
336
|
-
peerDependencies:
|
|
337
|
-
newConfig.peerDependencies || existingConfig.peerDependencies,
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
// Write the merged configuration back
|
|
341
|
-
const formattedConfig = JSON.stringify(mergedConfig, null, 2);
|
|
342
|
-
await fs.promises.writeFile(packageJsonPath, formattedConfig, 'utf8');
|
|
343
|
-
|
|
344
|
-
// Update the package in the database - use correct field names based on your Prisma schema
|
|
345
|
-
const updateData: Record<string, unknown> = {
|
|
346
|
-
lastUpdated: new Date(),
|
|
347
|
-
};
|
|
348
|
-
|
|
349
|
-
// Only update fields that exist in your Prisma schema
|
|
350
|
-
if (newConfig.version) updateData.version = newConfig.version;
|
|
351
|
-
if (newConfig.description !== undefined)
|
|
352
|
-
updateData.description = newConfig.description || '';
|
|
353
|
-
if (newConfig.license !== undefined)
|
|
354
|
-
updateData.license = newConfig.license || '';
|
|
355
|
-
if (newConfig.scripts)
|
|
356
|
-
updateData.scripts = JSON.stringify(newConfig.scripts);
|
|
357
|
-
if (newConfig.repository)
|
|
358
|
-
updateData.repository = JSON.stringify(newConfig.repository);
|
|
359
|
-
if (newConfig.dependencies)
|
|
360
|
-
updateData.dependencies = JSON.stringify(newConfig.dependencies);
|
|
361
|
-
if (newConfig.devDependencies)
|
|
362
|
-
updateData.devDependencies = JSON.stringify(newConfig.devDependencies);
|
|
363
|
-
if (newConfig.peerDependencies)
|
|
364
|
-
updateData.peerDependencies = JSON.stringify(newConfig.peerDependencies);
|
|
365
|
-
|
|
366
|
-
AppLogger.debug('Updating database with:', updateData);
|
|
367
|
-
|
|
368
|
-
const updatedPackage = await PackageRepository.updateConfig(packageName, updateData);
|
|
369
|
-
|
|
370
|
-
// Transform the response
|
|
371
|
-
const transformedPackage = {
|
|
372
|
-
...updatedPackage,
|
|
373
|
-
maintainers: updatedPackage.maintainers
|
|
374
|
-
? JSON.parse(updatedPackage.maintainers)
|
|
375
|
-
: [],
|
|
376
|
-
scripts: updatedPackage.scripts ? JSON.parse(updatedPackage.scripts) : {},
|
|
377
|
-
repository: updatedPackage.repository
|
|
378
|
-
? JSON.parse(updatedPackage.repository)
|
|
379
|
-
: {},
|
|
380
|
-
dependencies: updatedPackage.dependencies
|
|
381
|
-
? JSON.parse(updatedPackage.dependencies)
|
|
382
|
-
: {},
|
|
383
|
-
devDependencies: updatedPackage.devDependencies
|
|
384
|
-
? JSON.parse(updatedPackage.devDependencies)
|
|
385
|
-
: {},
|
|
386
|
-
peerDependencies: updatedPackage.peerDependencies
|
|
387
|
-
? JSON.parse(updatedPackage.peerDependencies)
|
|
388
|
-
: {},
|
|
389
|
-
};
|
|
390
|
-
return transformedPackage;
|
|
391
|
-
}
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GitService.ts
|
|
3
|
-
*
|
|
4
|
-
* This service executes native 'git' commands using Node.js's child_process
|
|
5
|
-
* to retrieve the commit history of the local repository.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { exec } from 'child_process';
|
|
9
|
-
import { promisify } from 'util';
|
|
10
|
-
import path from 'path';
|
|
11
|
-
|
|
12
|
-
import { AppLogger } from '../middleware/logger';
|
|
13
|
-
import type { Commit } from '../types';
|
|
14
|
-
import { VALID_COMMIT_TYPES } from '../types';
|
|
15
|
-
|
|
16
|
-
// Promisify the standard 'exec' function for easy async/await usage
|
|
17
|
-
const execPromise = promisify(exec);
|
|
18
|
-
|
|
19
|
-
export class GitService {
|
|
20
|
-
private repoPath: string;
|
|
21
|
-
|
|
22
|
-
constructor(repoPath: string = process.cwd()) {
|
|
23
|
-
this.repoPath = repoPath;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Retrieves commit history for the repository, optionally filtered by a package path.
|
|
28
|
-
*/
|
|
29
|
-
public async getAllCommits(pathFilter?: string): Promise<Commit[]> {
|
|
30
|
-
try {
|
|
31
|
-
|
|
32
|
-
let pathArgument = '';
|
|
33
|
-
if (pathFilter) {
|
|
34
|
-
// Normalize the path and ensure it's relative to the repo root
|
|
35
|
-
const normalizedPath = this.normalizePath(pathFilter);
|
|
36
|
-
if (normalizedPath) {
|
|
37
|
-
pathArgument = ` -C ${normalizedPath}`;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// First, validate we're in a git repo
|
|
42
|
-
await this.validateGitRepository(pathArgument);
|
|
43
|
-
|
|
44
|
-
AppLogger.debug(`Executing Git command in: ${this.repoPath}`);
|
|
45
|
-
// Use a simpler git log format
|
|
46
|
-
const command = `git log --pretty=format:"%H|%an|%ad|%s" --date=iso-strict ${pathArgument}`;
|
|
47
|
-
AppLogger.debug(`Git command: ${command}`);
|
|
48
|
-
|
|
49
|
-
const { stdout, stderr } = await execPromise(command, {
|
|
50
|
-
cwd: this.repoPath,
|
|
51
|
-
maxBuffer: 1024 * 5000,
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
if (stderr) {
|
|
55
|
-
AppLogger.warn('Git stderr: ' + stderr);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
if (!stdout.trim()) {
|
|
59
|
-
AppLogger.debug('No commits found for path: ' + pathFilter);
|
|
60
|
-
return [];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Parse the output
|
|
64
|
-
const commits: Commit[] = [];
|
|
65
|
-
const lines = stdout.trim().split('\n');
|
|
66
|
-
|
|
67
|
-
for (const line of lines) {
|
|
68
|
-
try {
|
|
69
|
-
const [hash, author, date, message] = line.split('|');
|
|
70
|
-
|
|
71
|
-
const commit: Commit = {
|
|
72
|
-
hash: hash.trim(),
|
|
73
|
-
author: author.trim(),
|
|
74
|
-
packageName: pathFilter || 'root',
|
|
75
|
-
date: new Date(date.trim()),
|
|
76
|
-
message: message.trim(),
|
|
77
|
-
type: this.extractCommitType(message.trim()),
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
commits.push(commit);
|
|
81
|
-
} catch (parseError) {
|
|
82
|
-
AppLogger.error('Failed to parse commit line: ' + line, parseError as Error);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
AppLogger.debug(`Successfully parsed ${commits.length} commits`);
|
|
87
|
-
return commits;
|
|
88
|
-
} catch (error) {
|
|
89
|
-
AppLogger.error('Error in getAllCommits', error as Error);
|
|
90
|
-
throw error;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Normalize path to be relative to git repo root
|
|
96
|
-
*/
|
|
97
|
-
private normalizePath(inputPath: string): string {
|
|
98
|
-
// If it's an absolute path, make it relative to repo root
|
|
99
|
-
if (path.isAbsolute(inputPath)) {
|
|
100
|
-
return path.relative(this.repoPath, inputPath);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// If it's already relative, return as-is
|
|
104
|
-
return inputPath;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Extract commit type from message
|
|
109
|
-
*/
|
|
110
|
-
private extractCommitType(message: string): string {
|
|
111
|
-
try {
|
|
112
|
-
const typeMatch = message.match(/^(\w+)(\([^)]+\))?!?:/);
|
|
113
|
-
if (typeMatch) {
|
|
114
|
-
const rawType = typeMatch[1].toLowerCase();
|
|
115
|
-
if (VALID_COMMIT_TYPES.includes(rawType)) {
|
|
116
|
-
return rawType;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return 'other';
|
|
120
|
-
} catch (error) {
|
|
121
|
-
return 'other';
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Validate that we're in a git repository
|
|
127
|
-
*/
|
|
128
|
-
private async validateGitRepository(pathArgument: string): Promise<void> {
|
|
129
|
-
try {
|
|
130
|
-
await execPromise('git '+pathArgument+' rev-parse --is-inside-work-tree', {
|
|
131
|
-
cwd: this.repoPath,
|
|
132
|
-
});
|
|
133
|
-
AppLogger.debug('Valid git repository');
|
|
134
|
-
} catch (error) {
|
|
135
|
-
throw new Error(
|
|
136
|
-
'Not a git repository (or any of the parent directories)'
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
scanMonorepo,
|
|
3
|
-
calculatePackageHealth,
|
|
4
|
-
} from '../utils/utilities';
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
funCheckBuildStatus,
|
|
8
|
-
funCheckLintStatus,
|
|
9
|
-
funCheckSecurityAudit,
|
|
10
|
-
} from '../utils/monorepo-scanner';
|
|
11
|
-
|
|
12
|
-
import { AppLogger } from '../middleware/logger';
|
|
13
|
-
import { PackageHealthRepository, PackageRepository } from '../repositories';
|
|
14
|
-
import type { TransformedPackageHealth, HealthResponse, PackageHealthModel } from '../types/database';
|
|
15
|
-
|
|
16
|
-
// Track in-flight health refresh requests to prevent duplicates
|
|
17
|
-
let inFlightHealthRefresh: Promise<HealthResponse> | null = null;
|
|
18
|
-
|
|
19
|
-
export const getHealthSummaryService = async (): Promise<HealthResponse> => {
|
|
20
|
-
const packageHealthData = await PackageHealthRepository.findAll() as PackageHealthModel[];
|
|
21
|
-
AppLogger.debug('packageHealthData count: ' + packageHealthData.length);
|
|
22
|
-
|
|
23
|
-
// Transform the data to match the expected frontend format
|
|
24
|
-
const packages = packageHealthData.map((pkg: PackageHealthModel) => {
|
|
25
|
-
const health = {
|
|
26
|
-
buildStatus: pkg.packageBuildStatus,
|
|
27
|
-
testCoverage: pkg.packageTestCoverage,
|
|
28
|
-
lintStatus: pkg.packageLintStatus,
|
|
29
|
-
securityAudit: pkg.packageSecurity,
|
|
30
|
-
overallScore: pkg.packageOverallScore,
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
packageName: pkg.packageName,
|
|
35
|
-
health: health,
|
|
36
|
-
isHealthy: pkg.packageOverallScore >= 80,
|
|
37
|
-
};
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// Calculate summary statistics
|
|
41
|
-
const total = packages.length;
|
|
42
|
-
const healthy = packages.filter((pkg: TransformedPackageHealth) => pkg.isHealthy).length;
|
|
43
|
-
const unhealthy = packages.filter((pkg: TransformedPackageHealth) => !pkg.isHealthy).length;
|
|
44
|
-
const averageScore =
|
|
45
|
-
packages.length > 0
|
|
46
|
-
? packages.reduce((sum: number, pkg: TransformedPackageHealth) => sum + pkg.health.overallScore, 0) /
|
|
47
|
-
packages.length
|
|
48
|
-
: 0;
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
packages: packages,
|
|
52
|
-
summary: {
|
|
53
|
-
total: total,
|
|
54
|
-
healthy: healthy,
|
|
55
|
-
unhealthy: unhealthy,
|
|
56
|
-
averageScore: averageScore,
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export const healthRefreshService = async (rootDir: string) => {
|
|
62
|
-
// If a health refresh is already in progress, return the in-flight promise
|
|
63
|
-
if (inFlightHealthRefresh) {
|
|
64
|
-
AppLogger.info('Health refresh already in progress, returning cached promise');
|
|
65
|
-
return inFlightHealthRefresh;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Create and store the health refresh promise
|
|
69
|
-
inFlightHealthRefresh = (async () => {
|
|
70
|
-
try {
|
|
71
|
-
const packages = scanMonorepo(rootDir);
|
|
72
|
-
AppLogger.debug('packages count: ' + packages.length);
|
|
73
|
-
const healthMetrics = await Promise.all(
|
|
74
|
-
packages.map(async pkg => {
|
|
75
|
-
try {
|
|
76
|
-
// Await each health check function since they return promises
|
|
77
|
-
const buildStatus = await funCheckBuildStatus(pkg);
|
|
78
|
-
const testCoverage = 0; //await funCheckTestCoverage(pkg); // skip test coverage for now
|
|
79
|
-
const lintStatus = await funCheckLintStatus(pkg);
|
|
80
|
-
const securityAudit = await funCheckSecurityAudit(pkg);
|
|
81
|
-
// Calculate overall health score
|
|
82
|
-
const overallScore = calculatePackageHealth(
|
|
83
|
-
buildStatus,
|
|
84
|
-
testCoverage,
|
|
85
|
-
lintStatus,
|
|
86
|
-
securityAudit
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
const health = {
|
|
90
|
-
buildStatus: buildStatus,
|
|
91
|
-
testCoverage: testCoverage,
|
|
92
|
-
lintStatus: lintStatus,
|
|
93
|
-
securityAudit: securityAudit,
|
|
94
|
-
overallScore: overallScore.overallScore,
|
|
95
|
-
};
|
|
96
|
-
const packageStatus =
|
|
97
|
-
health.overallScore >= 80
|
|
98
|
-
? 'healthy'
|
|
99
|
-
: health.overallScore >= 60 && health.overallScore < 80
|
|
100
|
-
? 'warning'
|
|
101
|
-
: 'error';
|
|
102
|
-
|
|
103
|
-
AppLogger.debug(`${pkg.name}: ${packageStatus}`, health);
|
|
104
|
-
|
|
105
|
-
await PackageHealthRepository.upsert({
|
|
106
|
-
packageName: pkg.name,
|
|
107
|
-
packageOverallScore: overallScore.overallScore,
|
|
108
|
-
packageBuildStatus: buildStatus,
|
|
109
|
-
packageTestCoverage: testCoverage,
|
|
110
|
-
packageLintStatus: lintStatus,
|
|
111
|
-
packageSecurity: securityAudit,
|
|
112
|
-
packageDependencies: '',
|
|
113
|
-
});
|
|
114
|
-
// update related package status as well
|
|
115
|
-
await PackageRepository.updateStatus(pkg.name, packageStatus);
|
|
116
|
-
return {
|
|
117
|
-
packageName: pkg.name,
|
|
118
|
-
health,
|
|
119
|
-
isHealthy: health.overallScore >= 80,
|
|
120
|
-
};
|
|
121
|
-
} catch (error) {
|
|
122
|
-
return {
|
|
123
|
-
packageName: pkg.name,
|
|
124
|
-
health: {
|
|
125
|
-
"buildStatus": "",
|
|
126
|
-
"testCoverage": 0,
|
|
127
|
-
"lintStatus": "",
|
|
128
|
-
"securityAudit": "",
|
|
129
|
-
"overallScore": 0
|
|
130
|
-
},
|
|
131
|
-
isHealthy: false,
|
|
132
|
-
error: 'Failed to fetch health metrics1',
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
})
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
const result: HealthResponse = {
|
|
139
|
-
packages: healthMetrics.filter(h => !h.error),
|
|
140
|
-
summary: {
|
|
141
|
-
total: packages.length,
|
|
142
|
-
healthy: healthMetrics.filter(h => h.isHealthy).length,
|
|
143
|
-
unhealthy: healthMetrics.filter(h => !h.isHealthy).length,
|
|
144
|
-
averageScore:
|
|
145
|
-
healthMetrics.filter(h => h.health).length > 0
|
|
146
|
-
? healthMetrics
|
|
147
|
-
.filter(h => h.health)
|
|
148
|
-
.reduce((sum, h) => sum + h.health!.overallScore, 0) /
|
|
149
|
-
healthMetrics.filter(h => h.health).length
|
|
150
|
-
: 0,
|
|
151
|
-
},
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
return result;
|
|
155
|
-
} finally {
|
|
156
|
-
// Clear the in-flight promise after completion
|
|
157
|
-
inFlightHealthRefresh = null;
|
|
158
|
-
}
|
|
159
|
-
})();
|
|
160
|
-
|
|
161
|
-
return inFlightHealthRefresh;
|
|
162
|
-
}
|