@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.
- package/.eslintrc.cjs +15 -0
- package/dist/cli.js +98 -0
- package/dist/gitService.js +240 -0
- package/dist/index.js +1185 -0
- package/dist/utils/helpers.js +198 -0
- package/package.json +41 -0
- package/prisma/migrations/20251017041048_init/migration.sql +19 -0
- package/prisma/migrations/20251017083007_add_package/migration.sql +21 -0
- package/prisma/migrations/20251021083705_alter_package/migration.sql +37 -0
- package/prisma/migrations/20251022085155_test/migration.sql +2 -0
- package/prisma/migrations/20251022160841_/migration.sql +35 -0
- package/prisma/migrations/20251023130158_rename_column_name/migration.sql +34 -0
- package/prisma/migrations/20251023174837_/migration.sql +34 -0
- package/prisma/migrations/20251023175830_uodate_schema/migration.sql +32 -0
- package/prisma/migrations/20251024103700_add_dependency_info/migration.sql +13 -0
- package/prisma/migrations/20251025192150_add_dependency_info/migration.sql +19 -0
- package/prisma/migrations/20251025192342_add_dependency_info/migration.sql +40 -0
- package/prisma/migrations/20251025204613_add_dependency_info/migration.sql +8 -0
- package/prisma/migrations/20251026071336_add_dependency_info/migration.sql +25 -0
- package/prisma/migrations/20251027062626_add_commit/migration.sql +10 -0
- package/prisma/migrations/20251027062748_add_commit/migration.sql +23 -0
- package/prisma/migrations/20251027092741_add_commit/migration.sql +17 -0
- package/prisma/migrations/20251027112736_add_health_status/migration.sql +16 -0
- package/prisma/migrations/20251027140546_init_packages/migration.sql +16 -0
- package/prisma/migrations/20251029073436_added_package_heath_key/migration.sql +34 -0
- package/prisma/migrations/20251029073830_added_package_health_key/migration.sql +49 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +130 -0
- package/src/cli.ts +68 -0
- package/src/gitService.ts +274 -0
- package/src/index.ts +1366 -0
- package/src/utils/helpers.js +199 -0
- package/src/utils/helpers.js.map +1 -0
- package/src/utils/helpers.ts +223 -0
- package/tsconfig.json +15 -0
- package/tsconfig.o.json +29 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- CreateTable
|
|
2
|
+
CREATE TABLE "package_health" (
|
|
3
|
+
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
4
|
+
"packageName" TEXT NOT NULL,
|
|
5
|
+
"packageOverallScore" REAL NOT NULL,
|
|
6
|
+
"packageBuildStatus" TEXT NOT NULL,
|
|
7
|
+
"packageTestCoverage" REAL,
|
|
8
|
+
"packageLintStatus" TEXT NOT NULL,
|
|
9
|
+
"packageSecurity" TEXT NOT NULL,
|
|
10
|
+
"packageDependencies" TEXT NOT NULL,
|
|
11
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
12
|
+
"updatedAt" DATETIME NOT NULL
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
-- CreateIndex
|
|
16
|
+
CREATE UNIQUE INDEX "package_health_packageName_key" ON "package_health"("packageName");
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Warnings:
|
|
3
|
+
|
|
4
|
+
- Added the required column `packageHealthId` to the `Package` table without a default value. This is not possible if the table is not empty.
|
|
5
|
+
|
|
6
|
+
*/
|
|
7
|
+
-- RedefineTables
|
|
8
|
+
PRAGMA defer_foreign_keys=ON;
|
|
9
|
+
PRAGMA foreign_keys=OFF;
|
|
10
|
+
CREATE TABLE "new_Package" (
|
|
11
|
+
"name" TEXT NOT NULL PRIMARY KEY,
|
|
12
|
+
"version" TEXT NOT NULL,
|
|
13
|
+
"type" TEXT NOT NULL,
|
|
14
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
15
|
+
"lastUpdated" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
16
|
+
"dependencies" TEXT,
|
|
17
|
+
"maintainers" TEXT NOT NULL,
|
|
18
|
+
"path" TEXT NOT NULL,
|
|
19
|
+
"description" TEXT NOT NULL,
|
|
20
|
+
"license" TEXT NOT NULL,
|
|
21
|
+
"repository" TEXT,
|
|
22
|
+
"scripts" TEXT,
|
|
23
|
+
"status" TEXT NOT NULL DEFAULT '',
|
|
24
|
+
"devDependencies" TEXT,
|
|
25
|
+
"peerDependencies" TEXT,
|
|
26
|
+
"packageHealthId" INTEGER NOT NULL,
|
|
27
|
+
CONSTRAINT "Package_packageHealthId_fkey" FOREIGN KEY ("packageHealthId") REFERENCES "package_health" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
|
28
|
+
);
|
|
29
|
+
INSERT INTO "new_Package" ("createdAt", "dependencies", "description", "devDependencies", "lastUpdated", "license", "maintainers", "name", "path", "peerDependencies", "repository", "scripts", "status", "type", "version") SELECT "createdAt", "dependencies", "description", "devDependencies", "lastUpdated", "license", "maintainers", "name", "path", "peerDependencies", "repository", "scripts", "status", "type", "version" FROM "Package";
|
|
30
|
+
DROP TABLE "Package";
|
|
31
|
+
ALTER TABLE "new_Package" RENAME TO "Package";
|
|
32
|
+
CREATE UNIQUE INDEX "Package_name_key" ON "Package"("name");
|
|
33
|
+
PRAGMA foreign_keys=ON;
|
|
34
|
+
PRAGMA defer_foreign_keys=OFF;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Warnings:
|
|
3
|
+
|
|
4
|
+
- You are about to drop the column `packageHealthId` on the `Package` table. All the data in the column will be lost.
|
|
5
|
+
|
|
6
|
+
*/
|
|
7
|
+
-- RedefineTables
|
|
8
|
+
PRAGMA defer_foreign_keys=ON;
|
|
9
|
+
PRAGMA foreign_keys=OFF;
|
|
10
|
+
CREATE TABLE "new_Package" (
|
|
11
|
+
"name" TEXT NOT NULL PRIMARY KEY,
|
|
12
|
+
"version" TEXT NOT NULL,
|
|
13
|
+
"type" TEXT NOT NULL,
|
|
14
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
15
|
+
"lastUpdated" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
16
|
+
"dependencies" TEXT,
|
|
17
|
+
"maintainers" TEXT NOT NULL,
|
|
18
|
+
"path" TEXT NOT NULL,
|
|
19
|
+
"description" TEXT NOT NULL,
|
|
20
|
+
"license" TEXT NOT NULL,
|
|
21
|
+
"repository" TEXT,
|
|
22
|
+
"scripts" TEXT,
|
|
23
|
+
"status" TEXT NOT NULL DEFAULT '',
|
|
24
|
+
"devDependencies" TEXT,
|
|
25
|
+
"peerDependencies" TEXT
|
|
26
|
+
);
|
|
27
|
+
INSERT INTO "new_Package" ("createdAt", "dependencies", "description", "devDependencies", "lastUpdated", "license", "maintainers", "name", "path", "peerDependencies", "repository", "scripts", "status", "type", "version") SELECT "createdAt", "dependencies", "description", "devDependencies", "lastUpdated", "license", "maintainers", "name", "path", "peerDependencies", "repository", "scripts", "status", "type", "version" FROM "Package";
|
|
28
|
+
DROP TABLE "Package";
|
|
29
|
+
ALTER TABLE "new_Package" RENAME TO "Package";
|
|
30
|
+
CREATE UNIQUE INDEX "Package_name_key" ON "Package"("name");
|
|
31
|
+
CREATE TABLE "new_package_health" (
|
|
32
|
+
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
|
33
|
+
"packageName" TEXT NOT NULL,
|
|
34
|
+
"packageOverallScore" REAL NOT NULL,
|
|
35
|
+
"packageBuildStatus" TEXT NOT NULL,
|
|
36
|
+
"packageTestCoverage" REAL,
|
|
37
|
+
"packageLintStatus" TEXT NOT NULL,
|
|
38
|
+
"packageSecurity" TEXT NOT NULL,
|
|
39
|
+
"packageDependencies" TEXT NOT NULL,
|
|
40
|
+
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
41
|
+
"updatedAt" DATETIME NOT NULL,
|
|
42
|
+
CONSTRAINT "package_health_packageName_fkey" FOREIGN KEY ("packageName") REFERENCES "Package" ("name") ON DELETE RESTRICT ON UPDATE CASCADE
|
|
43
|
+
);
|
|
44
|
+
INSERT INTO "new_package_health" ("createdAt", "id", "packageBuildStatus", "packageDependencies", "packageLintStatus", "packageName", "packageOverallScore", "packageSecurity", "packageTestCoverage", "updatedAt") SELECT "createdAt", "id", "packageBuildStatus", "packageDependencies", "packageLintStatus", "packageName", "packageOverallScore", "packageSecurity", "packageTestCoverage", "updatedAt" FROM "package_health";
|
|
45
|
+
DROP TABLE "package_health";
|
|
46
|
+
ALTER TABLE "new_package_health" RENAME TO "package_health";
|
|
47
|
+
CREATE UNIQUE INDEX "package_health_packageName_key" ON "package_health"("packageName");
|
|
48
|
+
PRAGMA foreign_keys=ON;
|
|
49
|
+
PRAGMA defer_foreign_keys=OFF;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// This is your Prisma schema file,
|
|
2
|
+
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
|
3
|
+
|
|
4
|
+
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
|
|
5
|
+
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
|
|
6
|
+
|
|
7
|
+
generator client {
|
|
8
|
+
provider = "prisma-client-js"
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
datasource db {
|
|
12
|
+
provider = "sqlite"
|
|
13
|
+
url = env("DATABASE_URL")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
model User {
|
|
17
|
+
id Int @id @default(autoincrement())
|
|
18
|
+
name String
|
|
19
|
+
email String @unique
|
|
20
|
+
posts Post[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
model Post {
|
|
24
|
+
id Int @id @default(autoincrement())
|
|
25
|
+
title String
|
|
26
|
+
content String?
|
|
27
|
+
published Boolean @default(false)
|
|
28
|
+
authorId Int
|
|
29
|
+
author User @relation(fields: [authorId], references: [id])
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// --- MONOREPO DATA MODEL ---
|
|
33
|
+
|
|
34
|
+
/// Represents a single package within the monorepo, based on the pnpm/package.json data.
|
|
35
|
+
model Package {
|
|
36
|
+
// Primary Key and Identity Field (using the package name as the unique ID)
|
|
37
|
+
// Example: '@monodog/dashboard'
|
|
38
|
+
name String @id @unique
|
|
39
|
+
|
|
40
|
+
// Core Package Metadata
|
|
41
|
+
version String
|
|
42
|
+
type String // e.g., 'app', 'package'
|
|
43
|
+
|
|
44
|
+
// Timestamps
|
|
45
|
+
createdAt DateTime @default(now())
|
|
46
|
+
lastUpdated DateTime @default(now())
|
|
47
|
+
|
|
48
|
+
// Key Metrics and Relationships
|
|
49
|
+
dependencies String? // The total number of direct dependencies
|
|
50
|
+
// Manual Serialization Required: Stores a JSON array string of maintainers, e.g., '["team-frontend"]'
|
|
51
|
+
maintainers String
|
|
52
|
+
// Manual Serialization Required: Stores a JSON array string of tags, e.g., '["core", "ui"]'
|
|
53
|
+
path String // The relative path in the file system, e.g., 'apps/dashboard'
|
|
54
|
+
|
|
55
|
+
// Descriptions and Configuration
|
|
56
|
+
description String
|
|
57
|
+
license String
|
|
58
|
+
repository String?
|
|
59
|
+
|
|
60
|
+
// Manual Serialization Required: Stores the scripts object as a JSON string
|
|
61
|
+
// Example: '{"dev": "vite", "build": "tsc && vite build"}'
|
|
62
|
+
scripts String?
|
|
63
|
+
status String @default("")
|
|
64
|
+
|
|
65
|
+
devDependencies String?
|
|
66
|
+
peerDependencies String?
|
|
67
|
+
dependenciesInfo DependencyInfo[]
|
|
68
|
+
commits Commit[]
|
|
69
|
+
packageHealth PackageHealth?
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
model DependencyInfo {
|
|
73
|
+
name String
|
|
74
|
+
packageName String
|
|
75
|
+
version String
|
|
76
|
+
type String @default("")
|
|
77
|
+
status String @default("")
|
|
78
|
+
latest String?
|
|
79
|
+
outdated Boolean @default(false)
|
|
80
|
+
package Package @relation(fields: [packageName], references: [name])
|
|
81
|
+
|
|
82
|
+
@@unique([name, packageName]) // Composite unique constraint
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
model Commit {
|
|
86
|
+
hash String @id
|
|
87
|
+
message String
|
|
88
|
+
author String
|
|
89
|
+
date DateTime?
|
|
90
|
+
type String
|
|
91
|
+
packageName String
|
|
92
|
+
package Package @relation(fields: [packageName], references: [name])
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
model HealthStatus {
|
|
96
|
+
id Int @id @default(autoincrement())
|
|
97
|
+
|
|
98
|
+
// Package reference (without formal relation)
|
|
99
|
+
packageName String @unique
|
|
100
|
+
// package Package @relation(fields: [packageName], references: [name], onDelete: Cascade)
|
|
101
|
+
|
|
102
|
+
// Health Metrics
|
|
103
|
+
overallScore Float // Overall health score (0-100)
|
|
104
|
+
buildStatus String // e.g., "passing", "failing", "unknown"
|
|
105
|
+
testCoverage Float // Test coverage percentage (0-100)
|
|
106
|
+
lintStatus String // e.g., "passing", "warning", "failing"
|
|
107
|
+
security String // e.g., "secure", "vulnerabilities", "unknown"
|
|
108
|
+
dependencies String // e.g., "up-to-date", "outdated", "vulnerable"
|
|
109
|
+
// Timestamps
|
|
110
|
+
createdAt DateTime @default(now())
|
|
111
|
+
updatedAt DateTime @updatedAt
|
|
112
|
+
|
|
113
|
+
@@map("health_status") // Optional: to specify the table name
|
|
114
|
+
}
|
|
115
|
+
model PackageHealth {
|
|
116
|
+
id Int @id @default(autoincrement())
|
|
117
|
+
packageName String @unique
|
|
118
|
+
packageOverallScore Float
|
|
119
|
+
packageBuildStatus String
|
|
120
|
+
packageTestCoverage Float?
|
|
121
|
+
packageLintStatus String
|
|
122
|
+
packageSecurity String // Changed from securityAudit to packageSecurity
|
|
123
|
+
packageDependencies String // Changed from dependencies to packageDependencies
|
|
124
|
+
createdAt DateTime @default(now())
|
|
125
|
+
updatedAt DateTime @updatedAt
|
|
126
|
+
package Package @relation(fields: [packageName], references: [name])
|
|
127
|
+
|
|
128
|
+
@@map("package_health")
|
|
129
|
+
}
|
|
130
|
+
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI Entry Point for the Monorepo Analysis Engine.
|
|
5
|
+
* * This script is executed when a user runs the `monodog-cli` command
|
|
6
|
+
* in their project. It handles command-line arguments to determine
|
|
7
|
+
* whether to:
|
|
8
|
+
* 1. Start the API server for the dashboard.
|
|
9
|
+
* 2. Run a one-off analysis command. (Future functionality)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
import { startServer } from './index'; // Assume index.ts exports this function
|
|
14
|
+
|
|
15
|
+
// --- Argument Parsing ---
|
|
16
|
+
|
|
17
|
+
// 1. Get arguments excluding the node executable and script name
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
|
|
20
|
+
// Default settings
|
|
21
|
+
let serve = false;
|
|
22
|
+
let rootPath = process.cwd(); // Default to the current working directory
|
|
23
|
+
|
|
24
|
+
// Simple argument parsing loop
|
|
25
|
+
for (let i = 0; i < args.length; i++) {
|
|
26
|
+
const arg = args[i];
|
|
27
|
+
|
|
28
|
+
if (arg === '--serve') {
|
|
29
|
+
serve = true;
|
|
30
|
+
} else if (arg === '--root') {
|
|
31
|
+
// Look at the next argument for the path
|
|
32
|
+
if (i + 1 < args.length) {
|
|
33
|
+
rootPath = path.resolve(args[i + 1]);
|
|
34
|
+
i++; // Skip the next argument since we've consumed it
|
|
35
|
+
} else {
|
|
36
|
+
console.error('Error: --root requires a path argument.');
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
} else if (arg === '-h' || arg === '--help') {
|
|
40
|
+
console.log(`
|
|
41
|
+
Monodog CLI - Monorepo Analysis Engine
|
|
42
|
+
|
|
43
|
+
Usage:
|
|
44
|
+
monodog-cli [options]
|
|
45
|
+
|
|
46
|
+
Options:
|
|
47
|
+
--serve Start the Monorepo Dashboard API server (default: off).
|
|
48
|
+
--root <path> Specify the root directory of the monorepo to analyze (default: current working directory).
|
|
49
|
+
-h, --help Show this help message.
|
|
50
|
+
|
|
51
|
+
Example:
|
|
52
|
+
monodog-cli --serve --root /path/to/my/monorepo
|
|
53
|
+
`);
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// --- Execution Logic ---
|
|
59
|
+
|
|
60
|
+
if (serve) {
|
|
61
|
+
console.log(`Starting Monodog API server...`);
|
|
62
|
+
console.log(`Analyzing monorepo at root: ${rootPath}`);
|
|
63
|
+
// Start the Express server and begin analysis
|
|
64
|
+
startServer(rootPath);
|
|
65
|
+
} else {
|
|
66
|
+
// Default mode: print usage or run a default report if no command is specified
|
|
67
|
+
console.log(`Monodog CLI: No operation specified. Use --serve to start the API or -h for help.`);
|
|
68
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
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
|
+
// Promisify the standard 'exec' function for easy async/await usage
|
|
13
|
+
const execPromise = promisify(exec);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Interface representing a single commit object with key metadata.
|
|
17
|
+
*/
|
|
18
|
+
interface GitCommit {
|
|
19
|
+
hash: string;
|
|
20
|
+
author: string;
|
|
21
|
+
packageName: string;
|
|
22
|
+
date: Date;
|
|
23
|
+
message: string;
|
|
24
|
+
type: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* List of standard Conventional Commit types for validation.
|
|
28
|
+
* Any extracted type not in this list will be set to 'other'.
|
|
29
|
+
*/
|
|
30
|
+
const VALID_COMMIT_TYPES: string[] = [
|
|
31
|
+
'feat', // New feature
|
|
32
|
+
'fix', // Bug fix
|
|
33
|
+
'docs', // Documentation changes
|
|
34
|
+
'style', // Code style changes (formatting, etc)
|
|
35
|
+
'refactor', // Code refactoring
|
|
36
|
+
'perf', // Performance improvements
|
|
37
|
+
'test', // Adding or updating tests
|
|
38
|
+
'chore', // Maintenance tasks (e.g., build scripts, dependency updates)
|
|
39
|
+
'ci', // CI/CD changes
|
|
40
|
+
'build', // Build system changes (e.g., pnpm/npm scripts)
|
|
41
|
+
'revert', // Reverting changes
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The delimiter used to separate individual commit records in the git log output.
|
|
46
|
+
* We use a unique string to ensure reliable splitting.
|
|
47
|
+
*/
|
|
48
|
+
// const COMMIT_DELIMITER = '|---COMMIT-END---|';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The custom format string passed to 'git log'.
|
|
52
|
+
* It uses JSON structure and our unique delimiter to make parsing consistent.
|
|
53
|
+
* * %H: Commit hash
|
|
54
|
+
* %pn: packageName name
|
|
55
|
+
* %ae: Author email
|
|
56
|
+
* %ad: Author date (ISO 8601 format)
|
|
57
|
+
* %s: Subject (commit message's first line - used for 'message' and 'type' fields)
|
|
58
|
+
*/
|
|
59
|
+
// const GIT_LOG_FORMAT = `{"hash": "%H", "author": "%ae", "packageName": "%pn", "date": "%ad", "message": "%s", "type": "%s"}${COMMIT_DELIMITER}`;
|
|
60
|
+
|
|
61
|
+
// export class GitService {
|
|
62
|
+
// private repoPath: string;
|
|
63
|
+
|
|
64
|
+
// constructor(repoPath: string = process.cwd()) {
|
|
65
|
+
// // Allows specifying a custom path to the monorepo root
|
|
66
|
+
// this.repoPath = repoPath;
|
|
67
|
+
// }
|
|
68
|
+
|
|
69
|
+
// /**
|
|
70
|
+
// * Retrieves commit history for the repository, optionally filtered by a package path.
|
|
71
|
+
// * @param pathFilter The path to a package directory (e.g., 'packages/server').
|
|
72
|
+
// * If provided, only commits affecting that path are returned.
|
|
73
|
+
// * @returns A Promise resolving to an array of GitCommit objects.
|
|
74
|
+
// */
|
|
75
|
+
// public async getAllCommits(pathFilter?: string): Promise<GitCommit[]> {
|
|
76
|
+
// const pathArgument = pathFilter ? ` -- ${pathFilter}` : '';
|
|
77
|
+
|
|
78
|
+
// const command = `git log --pretty=format:'${GIT_LOG_FORMAT}' --date=iso-strict${pathArgument}`;
|
|
79
|
+
|
|
80
|
+
// try {
|
|
81
|
+
// console.log(`Executing Git command: ${command}`);
|
|
82
|
+
// const { stdout } = await execPromise(command, {
|
|
83
|
+
// cwd: this.repoPath,
|
|
84
|
+
// maxBuffer: 1024 * 5000,
|
|
85
|
+
// }); // Increase buffer for large repos
|
|
86
|
+
// // console.log(stdout)
|
|
87
|
+
// console.log(stdout, '--', this.repoPath);
|
|
88
|
+
// const escapedDelimiter = COMMIT_DELIMITER.replace(
|
|
89
|
+
// /[.*+?^${}()|[\]\\]/g,
|
|
90
|
+
// '\\$&'
|
|
91
|
+
// );
|
|
92
|
+
|
|
93
|
+
// // 1. Remove the final trailing delimiter, as it creates an empty string at the end of the array
|
|
94
|
+
// const cleanedOutput = stdout
|
|
95
|
+
// .trim()
|
|
96
|
+
// .replace(new RegExp(`${escapedDelimiter}$`), '');
|
|
97
|
+
// console.log(cleanedOutput);
|
|
98
|
+
|
|
99
|
+
// if (!cleanedOutput) {
|
|
100
|
+
// return [];
|
|
101
|
+
// }
|
|
102
|
+
// // 2. Split the output by our custom delimiter to get an array of JSON strings
|
|
103
|
+
// const jsonStrings = cleanedOutput.split(COMMIT_DELIMITER);
|
|
104
|
+
// // 3. Parse each string into a GitCommit object
|
|
105
|
+
// const commits: GitCommit[] = jsonStrings
|
|
106
|
+
// .map(jsonString => {
|
|
107
|
+
// try {
|
|
108
|
+
// // JSON.parse is necessary because the output starts as a string
|
|
109
|
+
// const commit = JSON.parse(jsonString);
|
|
110
|
+
// console.log(commit.type, commit['hash']);
|
|
111
|
+
// try {
|
|
112
|
+
// // FIX: Updated Regex to handle optional scope (e.g., 'feat(scope):')
|
|
113
|
+
// // Matches:
|
|
114
|
+
// // 1. (^(\w+)) - The commit type (e.g., 'feat')
|
|
115
|
+
// // 2. (\([^)]+\))? - The optional scope (e.g., '(setup)')
|
|
116
|
+
// // 3. (:|!) - Ends with a colon or an exclamation point (for breaking changes)
|
|
117
|
+
// const typeMatch = commit.type.match(/^(\w+)(\([^)]+\))?([:!])/);
|
|
118
|
+
// let extractedType = 'other';
|
|
119
|
+
// if (typeMatch) {
|
|
120
|
+
// const rawType = typeMatch[1];
|
|
121
|
+
// // NEW: Validate the extracted type against the list of known types
|
|
122
|
+
// if (VALID_COMMIT_TYPES.includes(rawType)) {
|
|
123
|
+
// extractedType = rawType;
|
|
124
|
+
// }
|
|
125
|
+
// }
|
|
126
|
+
// commit.type = extractedType;
|
|
127
|
+
// } catch (e) {
|
|
128
|
+
// console.error('Failed to match commit type:', commit, e);
|
|
129
|
+
// // Skip malformed entries
|
|
130
|
+
// return null;
|
|
131
|
+
// }
|
|
132
|
+
// // // Set type to 'other' if the conventional format is not found
|
|
133
|
+
// // commit.type = typeMatch ? typeMatch[1] : 'other';
|
|
134
|
+
// return commit;
|
|
135
|
+
// } catch (e) {
|
|
136
|
+
// // console.log(jsonString)
|
|
137
|
+
// console.error('Failed to parse commit JSON:', jsonString, e);
|
|
138
|
+
// // Skip malformed entries
|
|
139
|
+
// return null;
|
|
140
|
+
// }
|
|
141
|
+
// })
|
|
142
|
+
// .filter((commit): commit is GitCommit => commit !== null);
|
|
143
|
+
|
|
144
|
+
// return commits;
|
|
145
|
+
// } catch (error) {
|
|
146
|
+
// console.error('Error fetching Git history:', error);
|
|
147
|
+
// // In a real application, you would handle this gracefully (e.g., throwing a custom error)
|
|
148
|
+
// throw new Error(
|
|
149
|
+
// 'Failed to retrieve Git commit history. Is Git installed and is the path correct?'
|
|
150
|
+
// );
|
|
151
|
+
// }
|
|
152
|
+
// }
|
|
153
|
+
// }
|
|
154
|
+
|
|
155
|
+
export class GitService {
|
|
156
|
+
private repoPath: string;
|
|
157
|
+
|
|
158
|
+
constructor(repoPath: string = process.cwd()) {
|
|
159
|
+
this.repoPath = repoPath;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Retrieves commit history for the repository, optionally filtered by a package path.
|
|
164
|
+
*/
|
|
165
|
+
public async getAllCommits(pathFilter?: string): Promise<GitCommit[]> {
|
|
166
|
+
try {
|
|
167
|
+
// First, validate we're in a git repo
|
|
168
|
+
await this.validateGitRepository();
|
|
169
|
+
|
|
170
|
+
let pathArgument = '';
|
|
171
|
+
if (pathFilter) {
|
|
172
|
+
// Normalize the path and ensure it's relative to the repo root
|
|
173
|
+
const normalizedPath = this.normalizePath(pathFilter);
|
|
174
|
+
pathArgument = ` -- ${normalizedPath}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Use a simpler git log format
|
|
178
|
+
const command = `git log --pretty=format:"%H|%an|%ad|%s" --date=iso-strict${pathArgument}`;
|
|
179
|
+
|
|
180
|
+
console.log(`🔧 Executing Git command in: ${this.repoPath}`);
|
|
181
|
+
console.log(`📝 Git command: ${command}`);
|
|
182
|
+
|
|
183
|
+
const { stdout, stderr } = await execPromise(command, {
|
|
184
|
+
cwd: this.repoPath,
|
|
185
|
+
maxBuffer: 1024 * 5000,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
if (stderr) {
|
|
189
|
+
console.warn('Git stderr:', stderr);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!stdout.trim()) {
|
|
193
|
+
console.log('📭 No commits found for path:', pathFilter);
|
|
194
|
+
return [];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Parse the output
|
|
198
|
+
const commits: GitCommit[] = [];
|
|
199
|
+
const lines = stdout.trim().split('\n');
|
|
200
|
+
|
|
201
|
+
for (const line of lines) {
|
|
202
|
+
try {
|
|
203
|
+
const [hash, author, date, message] = line.split('|');
|
|
204
|
+
|
|
205
|
+
const commit: GitCommit = {
|
|
206
|
+
hash: hash.trim(),
|
|
207
|
+
author: author.trim(),
|
|
208
|
+
packageName: pathFilter || 'root',
|
|
209
|
+
date: new Date(date.trim()),
|
|
210
|
+
message: message.trim(),
|
|
211
|
+
type: this.extractCommitType(message.trim()),
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
commits.push(commit);
|
|
215
|
+
} catch (parseError) {
|
|
216
|
+
console.error('❌ Failed to parse commit line:', line, parseError);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
console.log(`✅ Successfully parsed ${commits.length} commits`);
|
|
221
|
+
return commits;
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.error('💥 Error in getAllCommits:', error);
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Normalize path to be relative to git repo root
|
|
230
|
+
*/
|
|
231
|
+
private normalizePath(inputPath: string): string {
|
|
232
|
+
// If it's an absolute path, make it relative to repo root
|
|
233
|
+
if (path.isAbsolute(inputPath)) {
|
|
234
|
+
return path.relative(this.repoPath, inputPath);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// If it's already relative, return as-is
|
|
238
|
+
return inputPath;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Extract commit type from message
|
|
243
|
+
*/
|
|
244
|
+
private extractCommitType(message: string): string {
|
|
245
|
+
try {
|
|
246
|
+
const typeMatch = message.match(/^(\w+)(\([^)]+\))?!?:/);
|
|
247
|
+
if (typeMatch) {
|
|
248
|
+
const rawType = typeMatch[1].toLowerCase();
|
|
249
|
+
if (VALID_COMMIT_TYPES.includes(rawType)) {
|
|
250
|
+
return rawType;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return 'other';
|
|
254
|
+
} catch (error) {
|
|
255
|
+
return 'other';
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Validate that we're in a git repository
|
|
261
|
+
*/
|
|
262
|
+
private async validateGitRepository(): Promise<void> {
|
|
263
|
+
try {
|
|
264
|
+
await execPromise('git rev-parse --is-inside-work-tree', {
|
|
265
|
+
cwd: this.repoPath,
|
|
266
|
+
});
|
|
267
|
+
console.log('✅ Valid git repository');
|
|
268
|
+
} catch (error) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
'Not a git repository (or any of the parent directories)'
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|