quackstack 1.0.23 ā 1.0.24
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/dist/cli.cjs +82 -0
- package/dist/commands/ingest.js +65 -3
- package/dist/commands/search.js +115 -11
- package/dist/lib/context-generator.js +106 -12
- package/dist/lib/database.js +104 -36
- package/dist/lib/git-history.js +312 -0
- package/package.json +1 -1
- package/prisma/schema.prisma +32 -3
package/dist/cli.cjs
CHANGED
|
@@ -12,6 +12,8 @@ const context_generator_js_1 = require("./lib/context-generator.js");
|
|
|
12
12
|
const readme_js_1 = require("./commands/readme.js");
|
|
13
13
|
const agents_js_1 = require("./commands/agents.js");
|
|
14
14
|
const ai_provider_js_1 = require("./lib/ai-provider.js");
|
|
15
|
+
const database_js_1 = require("./lib/database.js");
|
|
16
|
+
const git_history_js_1 = require("./lib/git-history.js");
|
|
15
17
|
const path_1 = __importDefault(require("path"));
|
|
16
18
|
const program = new commander_1.Command();
|
|
17
19
|
const PROJECT_NAME = path_1.default.basename(process.cwd());
|
|
@@ -85,4 +87,84 @@ program
|
|
|
85
87
|
}
|
|
86
88
|
await (0, repl_js_1.startREPL)(options.reindex, options.provider, options.model);
|
|
87
89
|
});
|
|
90
|
+
program
|
|
91
|
+
.command("authors")
|
|
92
|
+
.description("Show contributor statistics for this project")
|
|
93
|
+
.action(async () => {
|
|
94
|
+
if (!git_history_js_1.gitHistory.isRepository()) {
|
|
95
|
+
console.log(chalk_1.default.red("ā Not a git repository"));
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
console.log(chalk_1.default.cyan("\nš Contributor Statistics\n"));
|
|
99
|
+
const authors = await (0, database_js_1.getProjectAuthors)(PROJECT_NAME);
|
|
100
|
+
if (authors.length === 0) {
|
|
101
|
+
console.log(chalk_1.default.yellow("No contributor data found. Run 'quack --reindex' first."));
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
authors.forEach((author, i) => {
|
|
105
|
+
console.log(chalk_1.default.green(`${i + 1}. ${author.author}`) + chalk_1.default.gray(` (${author.email})`));
|
|
106
|
+
console.log(chalk_1.default.white(` ${author.totalCommits} commits | +${author.linesAdded}/-${author.linesRemoved} lines`));
|
|
107
|
+
if (author.recentActivity) {
|
|
108
|
+
const daysAgo = Math.floor((Date.now() - author.recentActivity.getTime()) / (1000 * 60 * 60 * 24));
|
|
109
|
+
console.log(chalk_1.default.gray(` Last active ${daysAgo} days ago`));
|
|
110
|
+
}
|
|
111
|
+
if (author.filesOwned.length > 0) {
|
|
112
|
+
console.log(chalk_1.default.gray(` Owns ${author.filesOwned.length} files`));
|
|
113
|
+
}
|
|
114
|
+
console.log();
|
|
115
|
+
});
|
|
116
|
+
console.log(chalk_1.default.cyan(`Total: ${authors.length} contributors\n`));
|
|
117
|
+
});
|
|
118
|
+
program
|
|
119
|
+
.command("recent")
|
|
120
|
+
.description("Show recently modified files")
|
|
121
|
+
.option("-d, --days <number>", "Number of days to look back", "7")
|
|
122
|
+
.action(async (options) => {
|
|
123
|
+
if (!git_history_js_1.gitHistory.isRepository()) {
|
|
124
|
+
console.log(chalk_1.default.red("ā Not a git repository"));
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
const days = parseInt(options.days);
|
|
128
|
+
console.log(chalk_1.default.cyan(`\nš Files modified in last ${days} days\n`));
|
|
129
|
+
const files = await (0, database_js_1.getRecentlyModifiedFiles)(PROJECT_NAME, days);
|
|
130
|
+
if (files.length === 0) {
|
|
131
|
+
console.log(chalk_1.default.yellow(`No files modified in last ${days} days (or run 'quack --reindex')`));
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
files.forEach((file, i) => {
|
|
135
|
+
const daysAgo = Math.floor((Date.now() - file.lastCommitDate.getTime()) / (1000 * 60 * 60 * 24));
|
|
136
|
+
console.log(chalk_1.default.green(`${i + 1}. ${file.filePath}`));
|
|
137
|
+
console.log(chalk_1.default.gray(` Modified by ${file.lastCommitAuthor} ${daysAgo} days ago`));
|
|
138
|
+
if (file.lastCommitMessage) {
|
|
139
|
+
console.log(chalk_1.default.white(` "${file.lastCommitMessage.substring(0, 60)}${file.lastCommitMessage.length > 60 ? '...' : ''}"`));
|
|
140
|
+
}
|
|
141
|
+
console.log();
|
|
142
|
+
});
|
|
143
|
+
console.log(chalk_1.default.cyan(`Total: ${files.length} files\n`));
|
|
144
|
+
});
|
|
145
|
+
program
|
|
146
|
+
.command("git-info")
|
|
147
|
+
.description("Show git repository information")
|
|
148
|
+
.action(() => {
|
|
149
|
+
if (!git_history_js_1.gitHistory.isRepository()) {
|
|
150
|
+
console.log(chalk_1.default.red("ā Not a git repository"));
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
console.log(chalk_1.default.cyan("\nš Git Repository Info\n"));
|
|
154
|
+
const branch = git_history_js_1.gitHistory.getCurrentBranch();
|
|
155
|
+
if (branch) {
|
|
156
|
+
console.log(chalk_1.default.white(`Current Branch: `) + chalk_1.default.green(branch));
|
|
157
|
+
}
|
|
158
|
+
const repoRoot = git_history_js_1.gitHistory.getRepositoryRoot();
|
|
159
|
+
console.log(chalk_1.default.white(`Repository Root: `) + chalk_1.default.gray(repoRoot));
|
|
160
|
+
console.log(chalk_1.default.cyan("\nš Recent Commits:\n"));
|
|
161
|
+
const commits = git_history_js_1.gitHistory.getRecentCommits(10);
|
|
162
|
+
commits.slice(0, 5).forEach((commit, i) => {
|
|
163
|
+
const date = new Date(commit.date).toLocaleDateString();
|
|
164
|
+
console.log(chalk_1.default.green(`${i + 1}. ${commit.author}`) + chalk_1.default.gray(` (${date})`));
|
|
165
|
+
console.log(chalk_1.default.white(` ${commit.message.substring(0, 70)}${commit.message.length > 70 ? '...' : ''}`));
|
|
166
|
+
console.log(chalk_1.default.gray(` ${commit.filesChanged.length} files changed`));
|
|
167
|
+
console.log();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
88
170
|
program.parse();
|
package/dist/commands/ingest.js
CHANGED
|
@@ -2,14 +2,19 @@ import fs from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { scanDir } from "../lib/scanner.js";
|
|
4
4
|
import { chunkCode } from "../lib/chunker.js";
|
|
5
|
-
import { saveToDB } from "../lib/database.js";
|
|
5
|
+
import { saveToDB, saveAuthorToDB } from "../lib/database.js";
|
|
6
6
|
import { localEmbeddings } from "../lib/local-embeddings.js";
|
|
7
|
-
|
|
7
|
+
import { gitHistory } from "../lib/git-history.js";
|
|
8
|
+
export async function ingest(rootDir, projectName, silent = false, includeGitHistory = true) {
|
|
8
9
|
if (!silent)
|
|
9
10
|
console.log("Starting ingestion...");
|
|
10
11
|
const files = await scanDir(rootDir);
|
|
11
12
|
if (!silent)
|
|
12
13
|
console.log(`Found ${files.length} files to process`);
|
|
14
|
+
const isGitRepo = gitHistory.isRepository();
|
|
15
|
+
if (isGitRepo && includeGitHistory && !silent) {
|
|
16
|
+
console.log(`Git repository detected - enriching with history data`);
|
|
17
|
+
}
|
|
13
18
|
const allChunks = [];
|
|
14
19
|
for (const filePath of files) {
|
|
15
20
|
try {
|
|
@@ -31,10 +36,38 @@ export async function ingest(rootDir, projectName, silent = false) {
|
|
|
31
36
|
console.log(`Saving to database...`);
|
|
32
37
|
const BATCH_SIZE = 50;
|
|
33
38
|
let processedCount = 0;
|
|
39
|
+
const fileGitData = new Map();
|
|
40
|
+
if (isGitRepo && includeGitHistory) {
|
|
41
|
+
for (const { filePath } of allChunks) {
|
|
42
|
+
if (!fileGitData.has(filePath)) {
|
|
43
|
+
const history = gitHistory.getFileHistory(filePath, 100);
|
|
44
|
+
fileGitData.set(filePath, history);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
34
48
|
for (let i = 0; i < allChunks.length; i += BATCH_SIZE) {
|
|
35
49
|
const batch = allChunks.slice(i, i + BATCH_SIZE);
|
|
36
50
|
await Promise.all(batch.map(async ({ content, filePath, chunk }) => {
|
|
37
51
|
const embedding = localEmbeddings.getVector(content);
|
|
52
|
+
let gitMetadata = {};
|
|
53
|
+
if (isGitRepo && includeGitHistory) {
|
|
54
|
+
const history = fileGitData.get(filePath);
|
|
55
|
+
if (history && history.commits.length > 0) {
|
|
56
|
+
const lastCommit = history.commits[0];
|
|
57
|
+
const primaryAuthor = history.primaryAuthors[0];
|
|
58
|
+
gitMetadata = {
|
|
59
|
+
lastCommitHash: lastCommit.hash,
|
|
60
|
+
lastCommitAuthor: lastCommit.author,
|
|
61
|
+
lastCommitEmail: lastCommit.email,
|
|
62
|
+
lastCommitDate: lastCommit.date,
|
|
63
|
+
lastCommitMessage: lastCommit.message,
|
|
64
|
+
totalCommits: history.totalCommits,
|
|
65
|
+
primaryAuthor: primaryAuthor?.author,
|
|
66
|
+
primaryAuthorEmail: primaryAuthor?.email,
|
|
67
|
+
fileOwnerCommits: primaryAuthor?.commitCount,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
38
71
|
await saveToDB({
|
|
39
72
|
content,
|
|
40
73
|
embedding,
|
|
@@ -44,6 +77,7 @@ export async function ingest(rootDir, projectName, silent = false) {
|
|
|
44
77
|
functionName: chunk.functionName,
|
|
45
78
|
lineStart: chunk.lineStart,
|
|
46
79
|
lineEnd: chunk.lineEnd,
|
|
80
|
+
...gitMetadata,
|
|
47
81
|
});
|
|
48
82
|
}));
|
|
49
83
|
processedCount += batch.length;
|
|
@@ -51,6 +85,34 @@ export async function ingest(rootDir, projectName, silent = false) {
|
|
|
51
85
|
console.log(`Saved ${processedCount}/${allChunks.length} chunks...`);
|
|
52
86
|
}
|
|
53
87
|
}
|
|
54
|
-
if (!silent)
|
|
88
|
+
if (isGitRepo && includeGitHistory && !silent) {
|
|
89
|
+
console.log("Computing author statistics...");
|
|
90
|
+
const authorStats = gitHistory.getAuthorStats();
|
|
91
|
+
for (const stats of authorStats) {
|
|
92
|
+
const ownedFiles = [];
|
|
93
|
+
fileGitData.forEach((history, filePath) => {
|
|
94
|
+
if (history?.primaryAuthors[0]?.email === stats.email) {
|
|
95
|
+
ownedFiles.push(path.relative(gitHistory.getRepositoryRoot(), filePath));
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
await saveAuthorToDB({
|
|
99
|
+
projectName,
|
|
100
|
+
author: stats.author,
|
|
101
|
+
email: stats.email,
|
|
102
|
+
totalCommits: stats.totalCommits,
|
|
103
|
+
linesAdded: stats.linesAdded,
|
|
104
|
+
linesRemoved: stats.linesRemoved,
|
|
105
|
+
recentActivity: stats.recentActivity,
|
|
106
|
+
filesOwned: ownedFiles,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (!silent)
|
|
110
|
+
console.log(`Stored stats for ${authorStats.length} contributors`);
|
|
111
|
+
}
|
|
112
|
+
if (!silent) {
|
|
55
113
|
console.log(`Done! Processed ${processedCount} chunks from ${files.length} files.`);
|
|
114
|
+
if (isGitRepo && includeGitHistory) {
|
|
115
|
+
console.log(`Git history enrichment complete`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
56
118
|
}
|
package/dist/commands/search.js
CHANGED
|
@@ -2,21 +2,48 @@
|
|
|
2
2
|
import { client } from "../lib/database.js";
|
|
3
3
|
import { localEmbeddings } from "../lib/local-embeddings.js";
|
|
4
4
|
import { getAIClient } from "../lib/ai-provider.js";
|
|
5
|
-
export async function search(query, projectName, provider, model) {
|
|
5
|
+
export async function search(query, projectName, provider, model, options = {}) {
|
|
6
|
+
const { boostRecent = false, boostFrequent = false, filterAuthor, recentDays = 30, } = options;
|
|
7
|
+
const whereClause = { projectName };
|
|
8
|
+
if (filterAuthor) {
|
|
9
|
+
whereClause.OR = [
|
|
10
|
+
{ lastCommitEmail: filterAuthor },
|
|
11
|
+
{ primaryAuthorEmail: filterAuthor },
|
|
12
|
+
];
|
|
13
|
+
}
|
|
6
14
|
const snippets = await client.codeSnippet.findMany({
|
|
7
|
-
where:
|
|
15
|
+
where: whereClause,
|
|
8
16
|
});
|
|
9
17
|
const allContent = snippets.map(s => s.content);
|
|
10
18
|
localEmbeddings.addDocuments(allContent);
|
|
11
19
|
const queryVector = localEmbeddings.getVector(query);
|
|
12
20
|
const ranked = snippets
|
|
13
|
-
.map(snippet =>
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
.map(snippet => {
|
|
22
|
+
let score = localEmbeddings.cosineSimilarity(queryVector, snippet.embedding);
|
|
23
|
+
if (boostRecent && snippet.lastCommitDate) {
|
|
24
|
+
const daysSinceCommit = (Date.now() - snippet.lastCommitDate.getTime()) / (1000 * 60 * 60 * 24);
|
|
25
|
+
if (daysSinceCommit <= recentDays) {
|
|
26
|
+
const recencyBoost = 1 + (0.2 * (1 - daysSinceCommit / recentDays));
|
|
27
|
+
score *= recencyBoost;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (boostFrequent && snippet.totalCommits) {
|
|
31
|
+
const commitBoost = 1 + (Math.min(snippet.totalCommits, 50) / 50) * 0.15;
|
|
32
|
+
score *= commitBoost;
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
id: snippet.id,
|
|
36
|
+
content: snippet.content,
|
|
37
|
+
filePath: snippet.filePath,
|
|
38
|
+
functionName: snippet.functionName,
|
|
39
|
+
score,
|
|
40
|
+
lastCommitAuthor: snippet.lastCommitAuthor,
|
|
41
|
+
lastCommitDate: snippet.lastCommitDate,
|
|
42
|
+
lastCommitMessage: snippet.lastCommitMessage,
|
|
43
|
+
totalCommits: snippet.totalCommits,
|
|
44
|
+
primaryAuthor: snippet.primaryAuthor,
|
|
45
|
+
};
|
|
46
|
+
})
|
|
20
47
|
.sort((a, b) => b.score - a.score);
|
|
21
48
|
const seenFiles = new Set();
|
|
22
49
|
const uniqueResults = ranked.filter(item => {
|
|
@@ -26,9 +53,86 @@ export async function search(query, projectName, provider, model) {
|
|
|
26
53
|
return true;
|
|
27
54
|
}).slice(0, 5);
|
|
28
55
|
const context = uniqueResults
|
|
29
|
-
.map((r, i) =>
|
|
56
|
+
.map((r, i) => {
|
|
57
|
+
let entry = `[${i + 1}] ${r.filePath}${r.functionName ? ` (${r.functionName})` : ""}`;
|
|
58
|
+
if (r.lastCommitAuthor || r.primaryAuthor) {
|
|
59
|
+
const author = r.primaryAuthor || r.lastCommitAuthor;
|
|
60
|
+
entry += `\nPrimary Author: ${author}`;
|
|
61
|
+
if (r.totalCommits) {
|
|
62
|
+
entry += ` (${r.totalCommits} commits)`;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (r.lastCommitDate) {
|
|
66
|
+
const daysAgo = Math.floor((Date.now() - r.lastCommitDate.getTime()) / (1000 * 60 * 60 * 24));
|
|
67
|
+
entry += `\nLast Modified: ${daysAgo} days ago`;
|
|
68
|
+
if (r.lastCommitMessage) {
|
|
69
|
+
entry += `\nLast Commit: "${r.lastCommitMessage.substring(0, 60)}${r.lastCommitMessage.length > 60 ? '...' : ''}"`;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
entry += `\n\n${r.content}`;
|
|
73
|
+
return entry;
|
|
74
|
+
})
|
|
30
75
|
.join("\n\n---\n\n");
|
|
31
76
|
const aiClient = getAIClient(provider, model);
|
|
32
|
-
const
|
|
77
|
+
const enhancedPrompt = query +
|
|
78
|
+
"\n\nNote: Code snippets include git history metadata (authors, commit counts, last modified dates). " +
|
|
79
|
+
"Use this information to provide context about code ownership and recency when relevant.";
|
|
80
|
+
const answer = await aiClient.generateAnswer(enhancedPrompt, context);
|
|
33
81
|
return { answer, sources: uniqueResults };
|
|
34
82
|
}
|
|
83
|
+
export async function searchByAuthor(authorEmail, projectName) {
|
|
84
|
+
const snippets = await client.codeSnippet.findMany({
|
|
85
|
+
where: {
|
|
86
|
+
projectName,
|
|
87
|
+
OR: [
|
|
88
|
+
{ lastCommitEmail: authorEmail },
|
|
89
|
+
{ primaryAuthorEmail: authorEmail },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
select: {
|
|
93
|
+
filePath: true,
|
|
94
|
+
functionName: true,
|
|
95
|
+
totalCommits: true,
|
|
96
|
+
lastCommitDate: true,
|
|
97
|
+
},
|
|
98
|
+
orderBy: {
|
|
99
|
+
totalCommits: "desc",
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
const fileMap = new Map();
|
|
103
|
+
snippets.forEach(s => {
|
|
104
|
+
if (!fileMap.has(s.filePath)) {
|
|
105
|
+
fileMap.set(s.filePath, s);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
return Array.from(fileMap.values());
|
|
109
|
+
}
|
|
110
|
+
export async function getRecentActivity(projectName, days = 7) {
|
|
111
|
+
const since = new Date();
|
|
112
|
+
since.setDate(since.getDate() - days);
|
|
113
|
+
const snippets = await client.codeSnippet.findMany({
|
|
114
|
+
where: {
|
|
115
|
+
projectName,
|
|
116
|
+
lastCommitDate: {
|
|
117
|
+
gte: since,
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
select: {
|
|
121
|
+
filePath: true,
|
|
122
|
+
lastCommitAuthor: true,
|
|
123
|
+
lastCommitDate: true,
|
|
124
|
+
lastCommitMessage: true,
|
|
125
|
+
},
|
|
126
|
+
orderBy: {
|
|
127
|
+
lastCommitDate: "desc",
|
|
128
|
+
},
|
|
129
|
+
take: 20,
|
|
130
|
+
});
|
|
131
|
+
const fileMap = new Map();
|
|
132
|
+
snippets.forEach(s => {
|
|
133
|
+
if (!fileMap.has(s.filePath)) {
|
|
134
|
+
fileMap.set(s.filePath, s);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
return Array.from(fileMap.values());
|
|
138
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import os from "os";
|
|
4
|
-
import { search } from "../commands/search.js";
|
|
4
|
+
import { search, getRecentActivity } from "../commands/search.js";
|
|
5
|
+
import { getProjectAuthors } from "../lib/database.js";
|
|
6
|
+
import { gitHistory } from "../lib/git-history.js";
|
|
5
7
|
export async function generateCodebaseDoc(projectName) {
|
|
6
|
-
console.log("
|
|
8
|
+
console.log("Analyzing your codebase...\n");
|
|
7
9
|
const queries = [
|
|
8
10
|
"What is the overall architecture and design patterns used?",
|
|
9
11
|
"What are the main entry points and how does the application start?",
|
|
@@ -15,8 +17,24 @@ export async function generateCodebaseDoc(projectName) {
|
|
|
15
17
|
let doc = `# ${projectName} - Codebase Documentation\n\n`;
|
|
16
18
|
doc += `**Auto-generated by QuackStack** | Last updated: ${new Date().toLocaleString()}\n\n`;
|
|
17
19
|
doc += `This document provides a high-level overview of the codebase architecture, key components, and design decisions.\n\n`;
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
if (gitHistory.isRepository()) {
|
|
21
|
+
doc += `## Repository Information\n\n`;
|
|
22
|
+
const currentBranch = gitHistory.getCurrentBranch();
|
|
23
|
+
if (currentBranch) {
|
|
24
|
+
doc += `**Current Branch:** ${currentBranch}\n\n`;
|
|
25
|
+
}
|
|
26
|
+
const recentCommits = gitHistory.getRecentCommits(10);
|
|
27
|
+
if (recentCommits.length > 0) {
|
|
28
|
+
doc += `**Recent Activity:**\n`;
|
|
29
|
+
recentCommits.slice(0, 5).forEach(commit => {
|
|
30
|
+
const date = new Date(commit.date).toLocaleDateString();
|
|
31
|
+
doc += `- ${date} - ${commit.author}: ${commit.message.substring(0, 60)}${commit.message.length > 60 ? '...' : ''}\n`;
|
|
32
|
+
});
|
|
33
|
+
doc += `\n`;
|
|
34
|
+
}
|
|
35
|
+
doc += `---\n\n`;
|
|
36
|
+
}
|
|
37
|
+
doc += `## Table of Contents\n\n`;
|
|
20
38
|
queries.forEach((q, i) => {
|
|
21
39
|
const anchor = q.toLowerCase().replace(/[^a-z0-9]+/g, '-');
|
|
22
40
|
doc += `${i + 1}. [${q}](#${anchor})\n`;
|
|
@@ -28,12 +46,15 @@ export async function generateCodebaseDoc(projectName) {
|
|
|
28
46
|
doc += `## ${query}\n\n`;
|
|
29
47
|
doc += `${answer}\n\n`;
|
|
30
48
|
if (sources.length > 0) {
|
|
31
|
-
doc += `###
|
|
49
|
+
doc += `### Key Files\n\n`;
|
|
32
50
|
sources.slice(0, 5).forEach(s => {
|
|
33
51
|
doc += `- \`${s.filePath}\``;
|
|
34
52
|
if (s.functionName) {
|
|
35
53
|
doc += ` - ${s.functionName}`;
|
|
36
54
|
}
|
|
55
|
+
if (s.primaryAuthor) {
|
|
56
|
+
doc += ` (maintained by ${s.primaryAuthor})`;
|
|
57
|
+
}
|
|
37
58
|
doc += `\n`;
|
|
38
59
|
});
|
|
39
60
|
doc += `\n`;
|
|
@@ -44,7 +65,28 @@ export async function generateCodebaseDoc(projectName) {
|
|
|
44
65
|
console.error(`Error analyzing: ${query}`);
|
|
45
66
|
}
|
|
46
67
|
}
|
|
47
|
-
|
|
68
|
+
if (gitHistory.isRepository()) {
|
|
69
|
+
doc += `## Contributors\n\n`;
|
|
70
|
+
const authors = await getProjectAuthors(projectName);
|
|
71
|
+
if (authors.length > 0) {
|
|
72
|
+
doc += `This project has ${authors.length} contributor${authors.length > 1 ? 's' : ''}:\n\n`;
|
|
73
|
+
authors.slice(0, 10).forEach((author, i) => {
|
|
74
|
+
doc += `${i + 1}. **${author.author}** (${author.email})\n`;
|
|
75
|
+
doc += ` - ${author.totalCommits} commits\n`;
|
|
76
|
+
doc += ` - +${author.linesAdded} / -${author.linesRemoved} lines\n`;
|
|
77
|
+
if (author.recentActivity) {
|
|
78
|
+
const daysAgo = Math.floor((Date.now() - author.recentActivity.getTime()) / (1000 * 60 * 60 * 24));
|
|
79
|
+
doc += ` - Last active ${daysAgo} days ago\n`;
|
|
80
|
+
}
|
|
81
|
+
if (author.filesOwned.length > 0) {
|
|
82
|
+
doc += ` - Primary owner of ${author.filesOwned.length} files\n`;
|
|
83
|
+
}
|
|
84
|
+
doc += `\n`;
|
|
85
|
+
});
|
|
86
|
+
doc += `---\n\n`;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
doc += `## Project Structure\n\n`;
|
|
48
90
|
doc += '```\n';
|
|
49
91
|
doc += await getProjectStructure(process.cwd());
|
|
50
92
|
doc += '```\n\n';
|
|
@@ -59,7 +101,7 @@ export async function generateCodebaseDoc(projectName) {
|
|
|
59
101
|
doc += `### Running the Project\n`;
|
|
60
102
|
doc += `Refer to \`package.json\` scripts section for available commands.\n\n`;
|
|
61
103
|
doc += `---\n\n`;
|
|
62
|
-
doc += `##
|
|
104
|
+
doc += `## Updating This Document\n\n`;
|
|
63
105
|
doc += `This documentation is auto-generated. To regenerate:\n\n`;
|
|
64
106
|
doc += '```bash\n';
|
|
65
107
|
doc += 'quack --docs\n';
|
|
@@ -81,6 +123,34 @@ export async function generateContextFiles(projectName) {
|
|
|
81
123
|
let baseContext = `# ${projectName} - Codebase Context\n\n`;
|
|
82
124
|
baseContext += `Generated: ${new Date().toISOString()}\n\n`;
|
|
83
125
|
baseContext += "This file is auto-generated by QuackStack to provide AI assistants with codebase context.\n\n";
|
|
126
|
+
if (gitHistory.isRepository()) {
|
|
127
|
+
baseContext += `## Git Repository Info\n\n`;
|
|
128
|
+
const branch = gitHistory.getCurrentBranch();
|
|
129
|
+
if (branch) {
|
|
130
|
+
baseContext += `**Branch:** ${branch}\n\n`;
|
|
131
|
+
}
|
|
132
|
+
const recentActivity = await getRecentActivity(projectName, 7);
|
|
133
|
+
if (recentActivity.length > 0) {
|
|
134
|
+
baseContext += `**Recent Changes (Last 7 days):**\n\n`;
|
|
135
|
+
recentActivity.slice(0, 5).forEach(item => {
|
|
136
|
+
const daysAgo = Math.floor((Date.now() - item.lastCommitDate.getTime()) / (1000 * 60 * 60 * 24));
|
|
137
|
+
baseContext += `- ${item.filePath}\n`;
|
|
138
|
+
baseContext += ` - Modified by ${item.lastCommitAuthor} ${daysAgo} days ago\n`;
|
|
139
|
+
if (item.lastCommitMessage) {
|
|
140
|
+
baseContext += ` - "${item.lastCommitMessage.substring(0, 60)}${item.lastCommitMessage.length > 60 ? '...' : ''}"\n`;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
baseContext += `\n`;
|
|
144
|
+
}
|
|
145
|
+
const authors = await getProjectAuthors(projectName);
|
|
146
|
+
if (authors.length > 0) {
|
|
147
|
+
baseContext += `**Top Contributors:**\n\n`;
|
|
148
|
+
authors.slice(0, 5).forEach(author => {
|
|
149
|
+
baseContext += `- ${author.author} (${author.totalCommits} commits, owns ${author.filesOwned.length} files)\n`;
|
|
150
|
+
});
|
|
151
|
+
baseContext += `\n`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
84
154
|
baseContext += "---\n\n";
|
|
85
155
|
for (const query of queries) {
|
|
86
156
|
try {
|
|
@@ -89,7 +159,11 @@ export async function generateContextFiles(projectName) {
|
|
|
89
159
|
if (sources.length > 0) {
|
|
90
160
|
baseContext += "**Key files:**\n";
|
|
91
161
|
sources.slice(0, 3).forEach(s => {
|
|
92
|
-
baseContext += `- ${s.filePath}
|
|
162
|
+
baseContext += `- ${s.filePath}`;
|
|
163
|
+
if (s.primaryAuthor) {
|
|
164
|
+
baseContext += ` (${s.primaryAuthor})`;
|
|
165
|
+
}
|
|
166
|
+
baseContext += `\n`;
|
|
93
167
|
});
|
|
94
168
|
baseContext += "\n";
|
|
95
169
|
}
|
|
@@ -100,12 +174,28 @@ export async function generateContextFiles(projectName) {
|
|
|
100
174
|
}
|
|
101
175
|
baseContext += "---\n\n## Project Structure\n\n";
|
|
102
176
|
baseContext += await getProjectStructure(process.cwd());
|
|
177
|
+
if (gitHistory.isRepository()) {
|
|
178
|
+
const authors = await getProjectAuthors(projectName);
|
|
179
|
+
if (authors.length > 0) {
|
|
180
|
+
baseContext += "\n---\n\n## Code Ownership & Expertise\n\n";
|
|
181
|
+
baseContext += "When making changes, consider consulting these experts:\n\n";
|
|
182
|
+
for (const author of authors.slice(0, 5)) {
|
|
183
|
+
if (author.filesOwned.length > 0) {
|
|
184
|
+
baseContext += `**${author.author}** - Expert in:\n`;
|
|
185
|
+
author.filesOwned.slice(0, 5).forEach(file => {
|
|
186
|
+
baseContext += `- ${file}\n`;
|
|
187
|
+
});
|
|
188
|
+
baseContext += `\n`;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
103
193
|
await generateCursorRules(baseContext);
|
|
104
194
|
await generateWindsurfContext(baseContext);
|
|
105
195
|
await generateClineContext(baseContext);
|
|
106
196
|
await generateContinueContext(baseContext);
|
|
107
197
|
await generateAiderContext(baseContext);
|
|
108
|
-
console.log("\n
|
|
198
|
+
console.log("\n Context files generated for:");
|
|
109
199
|
console.log(" - Cursor (.cursorrules)");
|
|
110
200
|
console.log(" - Windsurf (.windsurfrules)");
|
|
111
201
|
console.log(" - Cline (.clinerules)");
|
|
@@ -138,11 +228,9 @@ async function generateAiderContext(context) {
|
|
|
138
228
|
const aiderConfig = `# Aider configuration with QuackStack context
|
|
139
229
|
# Project: ${path.basename(process.cwd())}
|
|
140
230
|
|
|
141
|
-
# Context file
|
|
142
231
|
read:
|
|
143
232
|
- .aider.context.md
|
|
144
233
|
|
|
145
|
-
# Model settings
|
|
146
234
|
model: gpt-4o-mini
|
|
147
235
|
edit-format: whole
|
|
148
236
|
`;
|
|
@@ -166,6 +254,12 @@ export async function updateGlobalContext(projectName) {
|
|
|
166
254
|
topFiles: sources.slice(0, 5).map(s => s.filePath),
|
|
167
255
|
lastUpdated: new Date().toISOString(),
|
|
168
256
|
};
|
|
257
|
+
if (gitHistory.isRepository()) {
|
|
258
|
+
const branch = gitHistory.getCurrentBranch();
|
|
259
|
+
const authors = await getProjectAuthors(projectName);
|
|
260
|
+
contexts[projectName].gitBranch = branch;
|
|
261
|
+
contexts[projectName].contributors = authors.length;
|
|
262
|
+
}
|
|
169
263
|
fs.writeFileSync(contextPath, JSON.stringify(contexts, null, 2), "utf-8");
|
|
170
264
|
}
|
|
171
265
|
export function watchAndUpdateContext(projectName) {
|
|
@@ -180,7 +274,7 @@ export function watchAndUpdateContext(projectName) {
|
|
|
180
274
|
await generateContextFiles(projectName);
|
|
181
275
|
}, 5000);
|
|
182
276
|
});
|
|
183
|
-
console.log("
|
|
277
|
+
console.log("Watching for file changes...");
|
|
184
278
|
}
|
|
185
279
|
async function getProjectStructure(dir, prefix = "", maxDepth = 3, currentDepth = 0) {
|
|
186
280
|
if (currentDepth >= maxDepth)
|
package/dist/lib/database.js
CHANGED
|
@@ -1,41 +1,109 @@
|
|
|
1
1
|
import { PrismaClient } from "@prisma/client";
|
|
2
2
|
export const client = new PrismaClient();
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
export async function saveToDB(data) {
|
|
4
|
+
await client.codeSnippet.create({
|
|
5
|
+
data: {
|
|
6
|
+
content: data.content,
|
|
7
|
+
embedding: data.embedding,
|
|
8
|
+
filePath: data.filePath,
|
|
9
|
+
projectName: data.projectName,
|
|
10
|
+
language: data.language,
|
|
11
|
+
functionName: data.functionName,
|
|
12
|
+
lineStart: data.lineStart,
|
|
13
|
+
lineEnd: data.lineEnd,
|
|
14
|
+
lastCommitHash: data.lastCommitHash,
|
|
15
|
+
lastCommitAuthor: data.lastCommitAuthor,
|
|
16
|
+
lastCommitEmail: data.lastCommitEmail,
|
|
17
|
+
lastCommitDate: data.lastCommitDate,
|
|
18
|
+
lastCommitMessage: data.lastCommitMessage,
|
|
19
|
+
totalCommits: data.totalCommits,
|
|
20
|
+
primaryAuthor: data.primaryAuthor,
|
|
21
|
+
primaryAuthorEmail: data.primaryAuthorEmail,
|
|
22
|
+
fileOwnerCommits: data.fileOwnerCommits,
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export async function saveAuthorToDB(data) {
|
|
27
|
+
await client.gitAuthor.upsert({
|
|
28
|
+
where: {
|
|
29
|
+
projectName_email: {
|
|
30
|
+
projectName: data.projectName,
|
|
31
|
+
email: data.email,
|
|
9
32
|
},
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
33
|
+
},
|
|
34
|
+
create: data,
|
|
35
|
+
update: {
|
|
36
|
+
author: data.author,
|
|
37
|
+
totalCommits: data.totalCommits,
|
|
38
|
+
linesAdded: data.linesAdded,
|
|
39
|
+
linesRemoved: data.linesRemoved,
|
|
40
|
+
recentActivity: data.recentActivity,
|
|
41
|
+
filesOwned: data.filesOwned,
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
export async function clearProject(projectName) {
|
|
46
|
+
await client.codeSnippet.deleteMany({
|
|
47
|
+
where: { projectName },
|
|
48
|
+
});
|
|
49
|
+
await client.gitAuthor.deleteMany({
|
|
50
|
+
where: { projectName },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export async function getProjectAuthors(projectName) {
|
|
54
|
+
return await client.gitAuthor.findMany({
|
|
55
|
+
where: { projectName },
|
|
56
|
+
orderBy: { totalCommits: "desc" },
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export async function getAuthorFiles(projectName, authorEmail) {
|
|
60
|
+
const snippets = await client.codeSnippet.findMany({
|
|
61
|
+
where: {
|
|
62
|
+
projectName,
|
|
63
|
+
primaryAuthorEmail: authorEmail,
|
|
64
|
+
},
|
|
65
|
+
select: {
|
|
66
|
+
filePath: true,
|
|
67
|
+
functionName: true,
|
|
68
|
+
totalCommits: true,
|
|
69
|
+
},
|
|
70
|
+
orderBy: {
|
|
71
|
+
totalCommits: "desc",
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
const fileMap = new Map();
|
|
75
|
+
snippets.forEach(s => {
|
|
76
|
+
if (!fileMap.has(s.filePath)) {
|
|
77
|
+
fileMap.set(s.filePath, s);
|
|
16
78
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
79
|
+
});
|
|
80
|
+
return Array.from(fileMap.values());
|
|
81
|
+
}
|
|
82
|
+
export async function getRecentlyModifiedFiles(projectName, days = 7) {
|
|
83
|
+
const since = new Date();
|
|
84
|
+
since.setDate(since.getDate() - days);
|
|
85
|
+
const snippets = await client.codeSnippet.findMany({
|
|
86
|
+
where: {
|
|
87
|
+
projectName,
|
|
88
|
+
lastCommitDate: {
|
|
89
|
+
gte: since,
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
select: {
|
|
93
|
+
filePath: true,
|
|
94
|
+
lastCommitAuthor: true,
|
|
95
|
+
lastCommitDate: true,
|
|
96
|
+
lastCommitMessage: true,
|
|
97
|
+
},
|
|
98
|
+
orderBy: {
|
|
99
|
+
lastCommitDate: "desc",
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
const fileMap = new Map();
|
|
103
|
+
snippets.forEach(s => {
|
|
104
|
+
if (!fileMap.has(s.filePath)) {
|
|
105
|
+
fileMap.set(s.filePath, s);
|
|
38
106
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}
|
|
107
|
+
});
|
|
108
|
+
return Array.from(fileMap.values());
|
|
109
|
+
}
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
export class GitHistory {
|
|
5
|
+
repoRoot;
|
|
6
|
+
isGitRepo;
|
|
7
|
+
constructor(projectPath = process.cwd()) {
|
|
8
|
+
this.repoRoot = this.findGitRoot(projectPath);
|
|
9
|
+
this.isGitRepo = this.repoRoot !== "";
|
|
10
|
+
}
|
|
11
|
+
findGitRoot(startPath) {
|
|
12
|
+
try {
|
|
13
|
+
const result = execSync("git rev-parse --show-toplevel", {
|
|
14
|
+
cwd: startPath,
|
|
15
|
+
encoding: "utf-8",
|
|
16
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
17
|
+
}).trim();
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
isRepository() {
|
|
25
|
+
return this.isGitRepo;
|
|
26
|
+
}
|
|
27
|
+
getFileHistory(filePath, limit = 10) {
|
|
28
|
+
if (!this.isGitRepo)
|
|
29
|
+
return null;
|
|
30
|
+
try {
|
|
31
|
+
const relativePath = path.relative(this.repoRoot, filePath);
|
|
32
|
+
// Get commit history for file
|
|
33
|
+
const logOutput = execSync(`git log --follow --format="%H|%an|%ae|%aI|%s" -n ${limit} -- "${relativePath}"`, {
|
|
34
|
+
cwd: this.repoRoot,
|
|
35
|
+
encoding: "utf-8",
|
|
36
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
37
|
+
}).trim();
|
|
38
|
+
if (!logOutput)
|
|
39
|
+
return null;
|
|
40
|
+
const commits = logOutput.split("\n").map(line => {
|
|
41
|
+
const [hash, author, email, date, ...messageParts] = line.split("|");
|
|
42
|
+
return {
|
|
43
|
+
hash,
|
|
44
|
+
author,
|
|
45
|
+
email,
|
|
46
|
+
date: new Date(date),
|
|
47
|
+
message: messageParts.join("|"),
|
|
48
|
+
filesChanged: [relativePath],
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
// Get author statistics
|
|
52
|
+
const authorMap = new Map();
|
|
53
|
+
commits.forEach(commit => {
|
|
54
|
+
const key = commit.email;
|
|
55
|
+
if (authorMap.has(key)) {
|
|
56
|
+
authorMap.get(key).commitCount++;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
authorMap.set(key, {
|
|
60
|
+
author: commit.author,
|
|
61
|
+
email: commit.email,
|
|
62
|
+
commitCount: 1,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
const primaryAuthors = Array.from(authorMap.values())
|
|
67
|
+
.sort((a, b) => b.commitCount - a.commitCount);
|
|
68
|
+
return {
|
|
69
|
+
filePath: relativePath,
|
|
70
|
+
commits,
|
|
71
|
+
lastModified: commits[0]?.date || new Date(),
|
|
72
|
+
lastAuthor: commits[0]?.author || "Unknown",
|
|
73
|
+
totalCommits: commits.length,
|
|
74
|
+
primaryAuthors,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
console.error(`Error getting history for ${filePath}:`, error);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
getBlameInfo(filePath) {
|
|
83
|
+
if (!this.isGitRepo)
|
|
84
|
+
return null;
|
|
85
|
+
try {
|
|
86
|
+
const relativePath = path.relative(this.repoRoot, filePath);
|
|
87
|
+
if (!fs.existsSync(filePath))
|
|
88
|
+
return null;
|
|
89
|
+
const blameOutput = execSync(`git blame --line-porcelain "${relativePath}"`, {
|
|
90
|
+
cwd: this.repoRoot,
|
|
91
|
+
encoding: "utf-8",
|
|
92
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
93
|
+
});
|
|
94
|
+
const lines = blameOutput.split("\n");
|
|
95
|
+
const blameInfo = [];
|
|
96
|
+
let currentInfo = {};
|
|
97
|
+
let lineNumber = 0;
|
|
98
|
+
for (const line of lines) {
|
|
99
|
+
if (line.match(/^[0-9a-f]{40}/)) {
|
|
100
|
+
if (currentInfo.commitHash) {
|
|
101
|
+
blameInfo.push(currentInfo);
|
|
102
|
+
}
|
|
103
|
+
currentInfo = {
|
|
104
|
+
commitHash: line.split(" ")[0],
|
|
105
|
+
lineNumber: ++lineNumber,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
else if (line.startsWith("author ")) {
|
|
109
|
+
currentInfo.author = line.substring(7);
|
|
110
|
+
}
|
|
111
|
+
else if (line.startsWith("author-mail ")) {
|
|
112
|
+
currentInfo.email = line.substring(12).replace(/[<>]/g, "");
|
|
113
|
+
}
|
|
114
|
+
else if (line.startsWith("author-time ")) {
|
|
115
|
+
currentInfo.date = new Date(parseInt(line.substring(12)) * 1000);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (currentInfo.commitHash) {
|
|
119
|
+
blameInfo.push(currentInfo);
|
|
120
|
+
}
|
|
121
|
+
return blameInfo;
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
console.error(`Error getting blame for ${filePath}:`, error);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
getRecentCommits(limit = 50) {
|
|
129
|
+
if (!this.isGitRepo)
|
|
130
|
+
return [];
|
|
131
|
+
try {
|
|
132
|
+
const logOutput = execSync(`git log --format="%H|%an|%ae|%aI|%s" --name-only -n ${limit}`, {
|
|
133
|
+
cwd: this.repoRoot,
|
|
134
|
+
encoding: "utf-8",
|
|
135
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
136
|
+
}).trim();
|
|
137
|
+
const commits = [];
|
|
138
|
+
const blocks = logOutput.split("\n\n");
|
|
139
|
+
for (const block of blocks) {
|
|
140
|
+
const lines = block.split("\n");
|
|
141
|
+
if (lines.length < 1)
|
|
142
|
+
continue;
|
|
143
|
+
const [hash, author, email, date, ...messageParts] = lines[0].split("|");
|
|
144
|
+
const filesChanged = lines.slice(1).filter(f => f.trim());
|
|
145
|
+
commits.push({
|
|
146
|
+
hash,
|
|
147
|
+
author,
|
|
148
|
+
email,
|
|
149
|
+
date: new Date(date),
|
|
150
|
+
message: messageParts.join("|"),
|
|
151
|
+
filesChanged,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return commits;
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
console.error("Error getting recent commits:", error);
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
getAuthorStats() {
|
|
162
|
+
if (!this.isGitRepo)
|
|
163
|
+
return [];
|
|
164
|
+
try {
|
|
165
|
+
// Get basic author stats
|
|
166
|
+
const authorOutput = execSync(`git shortlog -sne --all`, {
|
|
167
|
+
cwd: this.repoRoot,
|
|
168
|
+
encoding: "utf-8",
|
|
169
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
170
|
+
}).trim();
|
|
171
|
+
const authorMap = new Map();
|
|
172
|
+
authorOutput.split("\n").forEach(line => {
|
|
173
|
+
const match = line.match(/^\s*(\d+)\s+(.+?)\s+<(.+?)>/);
|
|
174
|
+
if (match) {
|
|
175
|
+
const [, commits, author, email] = match;
|
|
176
|
+
authorMap.set(email, {
|
|
177
|
+
author,
|
|
178
|
+
email,
|
|
179
|
+
totalCommits: parseInt(commits),
|
|
180
|
+
filesOwned: [],
|
|
181
|
+
recentActivity: new Date(0),
|
|
182
|
+
linesAdded: 0,
|
|
183
|
+
linesRemoved: 0,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
// Get recent activity for each author
|
|
188
|
+
authorMap.forEach((stats, email) => {
|
|
189
|
+
try {
|
|
190
|
+
const lastCommit = execSync(`git log --author="${email}" -1 --format="%aI"`, {
|
|
191
|
+
cwd: this.repoRoot,
|
|
192
|
+
encoding: "utf-8",
|
|
193
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
194
|
+
}).trim();
|
|
195
|
+
if (lastCommit) {
|
|
196
|
+
stats.recentActivity = new Date(lastCommit);
|
|
197
|
+
}
|
|
198
|
+
// Get line changes
|
|
199
|
+
const lineStats = execSync(`git log --author="${email}" --pretty=tformat: --numstat`, {
|
|
200
|
+
cwd: this.repoRoot,
|
|
201
|
+
encoding: "utf-8",
|
|
202
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
203
|
+
}).trim();
|
|
204
|
+
lineStats.split("\n").forEach(line => {
|
|
205
|
+
const [added, removed] = line.split("\t").map(n => parseInt(n) || 0);
|
|
206
|
+
stats.linesAdded += added;
|
|
207
|
+
stats.linesRemoved += removed;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// Ignore errors for individual authors
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
return Array.from(authorMap.values())
|
|
215
|
+
.sort((a, b) => b.totalCommits - a.totalCommits);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
console.error("Error getting author stats:", error);
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
getRecentlyChangedFiles(days = 7) {
|
|
223
|
+
if (!this.isGitRepo)
|
|
224
|
+
return [];
|
|
225
|
+
try {
|
|
226
|
+
const since = new Date();
|
|
227
|
+
since.setDate(since.getDate() - days);
|
|
228
|
+
const sinceStr = since.toISOString().split("T")[0];
|
|
229
|
+
const output = execSync(`git log --since="${sinceStr}" --format="%aI|%an" --name-only`, {
|
|
230
|
+
cwd: this.repoRoot,
|
|
231
|
+
encoding: "utf-8",
|
|
232
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
233
|
+
}).trim();
|
|
234
|
+
const fileMap = new Map();
|
|
235
|
+
const blocks = output.split("\n\n");
|
|
236
|
+
for (const block of blocks) {
|
|
237
|
+
const lines = block.split("\n");
|
|
238
|
+
if (lines.length < 1)
|
|
239
|
+
continue;
|
|
240
|
+
const [dateStr, author] = lines[0].split("|");
|
|
241
|
+
const date = new Date(dateStr);
|
|
242
|
+
const files = lines.slice(1).filter(f => f.trim());
|
|
243
|
+
files.forEach(file => {
|
|
244
|
+
if (!fileMap.has(file) || fileMap.get(file).lastModified < date) {
|
|
245
|
+
fileMap.set(file, { lastModified: date, author });
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
return Array.from(fileMap.entries())
|
|
250
|
+
.map(([filePath, info]) => ({ filePath, ...info }))
|
|
251
|
+
.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
console.error("Error getting recently changed files:", error);
|
|
255
|
+
return [];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
getFileOwner(filePath) {
|
|
259
|
+
const history = this.getFileHistory(filePath, 100);
|
|
260
|
+
if (!history || history.primaryAuthors.length === 0)
|
|
261
|
+
return null;
|
|
262
|
+
return history.primaryAuthors[0];
|
|
263
|
+
}
|
|
264
|
+
getCommitMessage(commitHash) {
|
|
265
|
+
if (!this.isGitRepo)
|
|
266
|
+
return null;
|
|
267
|
+
try {
|
|
268
|
+
return execSync(`git log --format=%B -n 1 ${commitHash}`, {
|
|
269
|
+
cwd: this.repoRoot,
|
|
270
|
+
encoding: "utf-8",
|
|
271
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
272
|
+
}).trim();
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
hasUncommittedChanges(filePath) {
|
|
279
|
+
if (!this.isGitRepo)
|
|
280
|
+
return false;
|
|
281
|
+
try {
|
|
282
|
+
const relativePath = path.relative(this.repoRoot, filePath);
|
|
283
|
+
const status = execSync(`git status --porcelain "${relativePath}"`, {
|
|
284
|
+
cwd: this.repoRoot,
|
|
285
|
+
encoding: "utf-8",
|
|
286
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
287
|
+
}).trim();
|
|
288
|
+
return status.length > 0;
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
getCurrentBranch() {
|
|
295
|
+
if (!this.isGitRepo)
|
|
296
|
+
return null;
|
|
297
|
+
try {
|
|
298
|
+
return execSync(`git rev-parse --abbrev-ref HEAD`, {
|
|
299
|
+
cwd: this.repoRoot,
|
|
300
|
+
encoding: "utf-8",
|
|
301
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
302
|
+
}).trim();
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
getRepositoryRoot() {
|
|
309
|
+
return this.repoRoot;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
export const gitHistory = new GitHistory();
|
package/package.json
CHANGED
package/prisma/schema.prisma
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
// This is your Prisma schema file,
|
|
2
2
|
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
|
3
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
4
|
generator client {
|
|
8
5
|
provider = "prisma-client-js"
|
|
9
6
|
}
|
|
@@ -23,8 +20,40 @@ model codeSnippet {
|
|
|
23
20
|
functionName String?
|
|
24
21
|
lineStart Int?
|
|
25
22
|
lineEnd Int?
|
|
23
|
+
|
|
24
|
+
lastCommitHash String?
|
|
25
|
+
lastCommitAuthor String?
|
|
26
|
+
lastCommitEmail String?
|
|
27
|
+
lastCommitDate DateTime?
|
|
28
|
+
lastCommitMessage String?
|
|
29
|
+
totalCommits Int? @default(0)
|
|
30
|
+
primaryAuthor String?
|
|
31
|
+
primaryAuthorEmail String?
|
|
32
|
+
fileOwnerCommits Int? @default(0)
|
|
33
|
+
|
|
34
|
+
createdAt DateTime @default(now())
|
|
35
|
+
updatedAt DateTime @updatedAt
|
|
36
|
+
|
|
37
|
+
@@index([projectName])
|
|
38
|
+
@@index([lastCommitDate])
|
|
39
|
+
@@index([primaryAuthor])
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
model gitAuthor {
|
|
43
|
+
id Int @id @default(autoincrement())
|
|
44
|
+
projectName String
|
|
45
|
+
author String
|
|
46
|
+
email String
|
|
47
|
+
totalCommits Int @default(0)
|
|
48
|
+
linesAdded Int @default(0)
|
|
49
|
+
linesRemoved Int @default(0)
|
|
50
|
+
recentActivity DateTime?
|
|
51
|
+
filesOwned String[]
|
|
52
|
+
|
|
26
53
|
createdAt DateTime @default(now())
|
|
27
54
|
updatedAt DateTime @updatedAt
|
|
28
55
|
|
|
56
|
+
@@unique([projectName, email])
|
|
29
57
|
@@index([projectName])
|
|
58
|
+
@@index([recentActivity])
|
|
30
59
|
}
|