commit-analyzer 1.0.2 ā 1.1.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/.claude/settings.local.json +11 -1
- package/README.md +33 -2
- package/commits.csv +2 -0
- package/eslint.config.mts +45 -0
- package/package.json +17 -9
- package/src/1.domain/analysis.ts +93 -0
- package/src/1.domain/analyzed-commit.ts +97 -0
- package/src/1.domain/application-error.ts +32 -0
- package/src/1.domain/category.ts +52 -0
- package/src/1.domain/commit-analysis-service.ts +92 -0
- package/src/1.domain/commit-hash.ts +40 -0
- package/src/1.domain/commit.ts +99 -0
- package/src/1.domain/date-formatting-service.ts +81 -0
- package/src/1.domain/date-range.ts +76 -0
- package/src/1.domain/report-generation-service.ts +292 -0
- package/src/2.application/analyze-commits.usecase.ts +307 -0
- package/src/2.application/generate-report.usecase.ts +204 -0
- package/src/2.application/llm-service.ts +54 -0
- package/src/2.application/resume-analysis.usecase.ts +123 -0
- package/src/3.presentation/analysis-repository.interface.ts +27 -0
- package/src/3.presentation/analyze-command.ts +128 -0
- package/src/3.presentation/cli-application.ts +255 -0
- package/src/3.presentation/command-handler.interface.ts +4 -0
- package/src/3.presentation/commit-analysis-controller.ts +101 -0
- package/src/3.presentation/commit-repository.interface.ts +47 -0
- package/src/3.presentation/console-formatter.ts +129 -0
- package/src/3.presentation/progress-repository.interface.ts +49 -0
- package/src/3.presentation/report-command.ts +50 -0
- package/src/3.presentation/resume-command.ts +59 -0
- package/src/3.presentation/storage-repository.interface.ts +33 -0
- package/src/3.presentation/storage-service.interface.ts +32 -0
- package/src/3.presentation/version-control-service.interface.ts +41 -0
- package/src/4.infrastructure/cache-service.ts +271 -0
- package/src/4.infrastructure/cached-analysis-repository.ts +46 -0
- package/src/4.infrastructure/claude-llm-adapter.ts +124 -0
- package/src/4.infrastructure/csv-service.ts +206 -0
- package/src/4.infrastructure/file-storage-repository.ts +108 -0
- package/src/4.infrastructure/file-system-storage-adapter.ts +87 -0
- package/src/4.infrastructure/gemini-llm-adapter.ts +46 -0
- package/src/4.infrastructure/git-adapter.ts +116 -0
- package/src/4.infrastructure/git-commit-repository.ts +85 -0
- package/src/4.infrastructure/json-progress-tracker.ts +182 -0
- package/src/4.infrastructure/llm-adapter-factory.ts +26 -0
- package/src/4.infrastructure/llm-adapter.ts +455 -0
- package/src/4.infrastructure/llm-analysis-repository.ts +38 -0
- package/src/4.infrastructure/openai-llm-adapter.ts +57 -0
- package/src/di.ts +108 -0
- package/src/main.ts +63 -0
- package/src/utils/app-paths.ts +36 -0
- package/src/utils/concurrency.ts +81 -0
- package/src/utils.ts +77 -0
- package/tsconfig.json +7 -1
- package/src/cli.ts +0 -170
- package/src/csv-reader.ts +0 -180
- package/src/csv.ts +0 -40
- package/src/errors.ts +0 -49
- package/src/git.ts +0 -112
- package/src/index.ts +0 -395
- package/src/llm.ts +0 -396
- package/src/progress.ts +0 -84
- package/src/report-generator.ts +0 -286
- package/src/types.ts +0 -24
package/src/index.ts
DELETED
|
@@ -1,395 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { GitService } from "./git"
|
|
4
|
-
import { LLMService } from "./llm"
|
|
5
|
-
import { CSVService } from "./csv"
|
|
6
|
-
import { CLIService } from "./cli"
|
|
7
|
-
import { ProgressTracker } from "./progress"
|
|
8
|
-
import { MarkdownReportGenerator } from "./report-generator"
|
|
9
|
-
import { handleError, GitError, ValidationError } from "./errors"
|
|
10
|
-
import { AnalyzedCommit } from "./types"
|
|
11
|
-
import * as readline from "readline"
|
|
12
|
-
|
|
13
|
-
async function promptResume(): Promise<boolean> {
|
|
14
|
-
const rl = readline.createInterface({
|
|
15
|
-
input: process.stdin,
|
|
16
|
-
output: process.stdout,
|
|
17
|
-
})
|
|
18
|
-
|
|
19
|
-
return new Promise((resolve) => {
|
|
20
|
-
rl.question(
|
|
21
|
-
"\nDo you want to resume from the checkpoint? (y/n): ",
|
|
22
|
-
(answer) => {
|
|
23
|
-
rl.close()
|
|
24
|
-
resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes")
|
|
25
|
-
},
|
|
26
|
-
)
|
|
27
|
-
})
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
async function main(): Promise<void> {
|
|
31
|
-
try {
|
|
32
|
-
if (!GitService.isGitRepository()) {
|
|
33
|
-
throw new GitError("Current directory is not a git repository")
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const options = CLIService.parseArguments()
|
|
37
|
-
|
|
38
|
-
// Resolve output file with output directory
|
|
39
|
-
if (options.output) {
|
|
40
|
-
options.output = CLIService.resolveOutputPath(
|
|
41
|
-
options.output,
|
|
42
|
-
options.outputDir,
|
|
43
|
-
)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Handle input CSV mode (skip commit analysis, just generate report)
|
|
47
|
-
if (options.inputCsv) {
|
|
48
|
-
console.log("Generating report from existing CSV...")
|
|
49
|
-
|
|
50
|
-
// Ensure --report flag is set when using --input-csv
|
|
51
|
-
if (!options.report) {
|
|
52
|
-
options.report = true
|
|
53
|
-
console.log(
|
|
54
|
-
"Note: --report flag automatically enabled when using --input-csv",
|
|
55
|
-
)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Determine output file name for report
|
|
59
|
-
let reportOutput =
|
|
60
|
-
options.output ||
|
|
61
|
-
CLIService.resolveOutputPath("report.md", options.outputDir)
|
|
62
|
-
if (
|
|
63
|
-
reportOutput.endsWith("commits.csv") ||
|
|
64
|
-
reportOutput.endsWith("/commits.csv")
|
|
65
|
-
) {
|
|
66
|
-
reportOutput = CLIService.resolveOutputPath(
|
|
67
|
-
"report.md",
|
|
68
|
-
options.outputDir,
|
|
69
|
-
)
|
|
70
|
-
} else if (!reportOutput.endsWith(".md")) {
|
|
71
|
-
// If user specified output but it's not .md, append .md
|
|
72
|
-
reportOutput = reportOutput.replace(/\.[^.]+$/, "") + ".md"
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
await MarkdownReportGenerator.generateReport(
|
|
76
|
-
options.inputCsv,
|
|
77
|
-
reportOutput,
|
|
78
|
-
)
|
|
79
|
-
return
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Prompt to select LLM model if not provided
|
|
83
|
-
const availableModels = LLMService.detectAvailableModels()
|
|
84
|
-
if (availableModels.length === 0) {
|
|
85
|
-
throw new Error(
|
|
86
|
-
"No supported LLM models found. Please install claude, gemini, or codex.",
|
|
87
|
-
)
|
|
88
|
-
}
|
|
89
|
-
const defaultModel = LLMService.detectDefaultModel()
|
|
90
|
-
let selectedModel = options.model
|
|
91
|
-
if (!selectedModel) {
|
|
92
|
-
const rlModel = readline.createInterface({
|
|
93
|
-
input: process.stdin,
|
|
94
|
-
output: process.stdout,
|
|
95
|
-
})
|
|
96
|
-
selectedModel = await new Promise<string>((resolve) =>
|
|
97
|
-
rlModel.question(
|
|
98
|
-
`Select LLM model (${availableModels.join("/")}) [${defaultModel}]: `,
|
|
99
|
-
(answer) => {
|
|
100
|
-
rlModel.close()
|
|
101
|
-
resolve(answer.trim() || defaultModel)
|
|
102
|
-
},
|
|
103
|
-
),
|
|
104
|
-
)
|
|
105
|
-
}
|
|
106
|
-
LLMService.setModel(selectedModel)
|
|
107
|
-
LLMService.setVerbose(options.verbose || false)
|
|
108
|
-
|
|
109
|
-
// Handle clear flag
|
|
110
|
-
if (options.clear) {
|
|
111
|
-
if (ProgressTracker.hasProgress()) {
|
|
112
|
-
ProgressTracker.clearProgress()
|
|
113
|
-
console.log("ā Progress checkpoint cleared")
|
|
114
|
-
} else {
|
|
115
|
-
console.log("No progress checkpoint to clear")
|
|
116
|
-
}
|
|
117
|
-
if (!options.resume) {
|
|
118
|
-
return
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
let commitsToAnalyze: string[] = options.commits
|
|
123
|
-
let analyzedCommits: AnalyzedCommit[] = []
|
|
124
|
-
let processedCommits: string[] = []
|
|
125
|
-
|
|
126
|
-
// Handle resume flag
|
|
127
|
-
if (options.resume && ProgressTracker.hasProgress()) {
|
|
128
|
-
const progressState = ProgressTracker.loadProgress()
|
|
129
|
-
if (progressState) {
|
|
130
|
-
console.log("š Found previous session checkpoint")
|
|
131
|
-
console.log(ProgressTracker.formatProgressSummary(progressState))
|
|
132
|
-
|
|
133
|
-
const resumeChoice = await promptResume()
|
|
134
|
-
if (resumeChoice) {
|
|
135
|
-
commitsToAnalyze = ProgressTracker.getRemainingCommits(progressState)
|
|
136
|
-
analyzedCommits = progressState.analyzedCommits
|
|
137
|
-
processedCommits = progressState.processedCommits
|
|
138
|
-
|
|
139
|
-
// Use the output file from the previous session
|
|
140
|
-
options.output = progressState.outputFile
|
|
141
|
-
|
|
142
|
-
console.log(
|
|
143
|
-
`\nā¶ļø Resuming with ${commitsToAnalyze.length} remaining commits...`,
|
|
144
|
-
)
|
|
145
|
-
console.log(
|
|
146
|
-
`š Previous progress: ${processedCommits.length}/${progressState.totalCommits.length} commits processed`,
|
|
147
|
-
)
|
|
148
|
-
if (options.verbose) {
|
|
149
|
-
console.log(
|
|
150
|
-
` Debug: analyzedCommits.length = ${analyzedCommits.length}`,
|
|
151
|
-
)
|
|
152
|
-
console.log(
|
|
153
|
-
` Debug: processedCommits.length = ${processedCommits.length}`,
|
|
154
|
-
)
|
|
155
|
-
console.log(
|
|
156
|
-
` Debug: commitsToAnalyze.length = ${commitsToAnalyze.length}`,
|
|
157
|
-
)
|
|
158
|
-
}
|
|
159
|
-
} else {
|
|
160
|
-
ProgressTracker.clearProgress()
|
|
161
|
-
console.log("Starting fresh analysis...")
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
} else if (options.resume && !ProgressTracker.hasProgress()) {
|
|
165
|
-
console.log("No previous checkpoint found. Starting fresh...")
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Only get new commits if not resuming
|
|
169
|
-
if (commitsToAnalyze.length === 0 || (!options.resume && !options.clear)) {
|
|
170
|
-
if (options.useDefaults) {
|
|
171
|
-
console.log("No commits specified, analyzing your authored commits...")
|
|
172
|
-
const userEmail = GitService.getCurrentUserEmail()
|
|
173
|
-
const userName = GitService.getCurrentUserName()
|
|
174
|
-
console.log(`Finding commits by ${userName} (${userEmail})`)
|
|
175
|
-
|
|
176
|
-
commitsToAnalyze = GitService.getUserAuthoredCommits(
|
|
177
|
-
options.author,
|
|
178
|
-
options.limit,
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
if (commitsToAnalyze.length === 0) {
|
|
182
|
-
throw new ValidationError(
|
|
183
|
-
"No commits found for the current user. Make sure you have commits in this repository.",
|
|
184
|
-
)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const limitText = options.limit ? ` (limited to ${options.limit})` : ""
|
|
188
|
-
console.log(`Found ${commitsToAnalyze.length} commits${limitText}`)
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const totalCommitsToProcess =
|
|
193
|
-
processedCommits.length + commitsToAnalyze.length
|
|
194
|
-
console.log(
|
|
195
|
-
`\nAnalyzing ${commitsToAnalyze.length} commits (${totalCommitsToProcess} total)...`,
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
let failedCommits = 0
|
|
199
|
-
|
|
200
|
-
// Keep track of all commits for checkpoint
|
|
201
|
-
const allCommitsToAnalyze = [...processedCommits, ...commitsToAnalyze]
|
|
202
|
-
|
|
203
|
-
for (const [index, hash] of commitsToAnalyze.entries()) {
|
|
204
|
-
const overallIndex = processedCommits.length + index + 1
|
|
205
|
-
console.log(
|
|
206
|
-
`\n[${overallIndex}/${totalCommitsToProcess}] Processing commit: ${hash.substring(0, 8)}`,
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
if (!GitService.validateCommitHash(hash)) {
|
|
210
|
-
console.error(` ā Invalid commit hash: ${hash}`)
|
|
211
|
-
failedCommits++
|
|
212
|
-
processedCommits.push(hash)
|
|
213
|
-
continue
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
try {
|
|
217
|
-
const commitInfo = await GitService.getCommitInfo(hash)
|
|
218
|
-
console.log(` ā Extracted commit info`)
|
|
219
|
-
|
|
220
|
-
const analysis = await LLMService.analyzeCommit(commitInfo)
|
|
221
|
-
console.log(
|
|
222
|
-
` ā Analyzed as "${analysis.category}": ${analysis.summary}`,
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
analyzedCommits.push({
|
|
226
|
-
...commitInfo,
|
|
227
|
-
analysis,
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
processedCommits.push(hash)
|
|
231
|
-
|
|
232
|
-
// Save progress every 10 commits or on failure
|
|
233
|
-
if (overallIndex % 10 === 0 || index === commitsToAnalyze.length - 1) {
|
|
234
|
-
ProgressTracker.saveProgress(
|
|
235
|
-
allCommitsToAnalyze,
|
|
236
|
-
processedCommits,
|
|
237
|
-
analyzedCommits,
|
|
238
|
-
options.output!,
|
|
239
|
-
)
|
|
240
|
-
console.log(
|
|
241
|
-
` š¾ Progress saved (${overallIndex}/${totalCommitsToProcess})`,
|
|
242
|
-
)
|
|
243
|
-
if (options.verbose) {
|
|
244
|
-
console.log(
|
|
245
|
-
` Debug: Saved ${processedCommits.length} processed, ${analyzedCommits.length} analyzed`,
|
|
246
|
-
)
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
} catch (error) {
|
|
250
|
-
const errorMessage =
|
|
251
|
-
error instanceof Error ? error.message : "Unknown error"
|
|
252
|
-
console.error(` ā Failed: ${errorMessage}`)
|
|
253
|
-
failedCommits++
|
|
254
|
-
processedCommits.push(hash)
|
|
255
|
-
|
|
256
|
-
// Check if this was a rate limit error and provide helpful messaging
|
|
257
|
-
const isRateLimitError =
|
|
258
|
-
errorMessage.includes("quota exceeded") ||
|
|
259
|
-
errorMessage.includes("rate limit") ||
|
|
260
|
-
errorMessage.includes("429")
|
|
261
|
-
|
|
262
|
-
// Save progress on failure
|
|
263
|
-
ProgressTracker.saveProgress(
|
|
264
|
-
allCommitsToAnalyze,
|
|
265
|
-
processedCommits,
|
|
266
|
-
analyzedCommits,
|
|
267
|
-
options.output!,
|
|
268
|
-
)
|
|
269
|
-
console.log(` š¾ Progress saved after failure`)
|
|
270
|
-
|
|
271
|
-
// Provide specific guidance based on error type
|
|
272
|
-
if (isRateLimitError) {
|
|
273
|
-
console.error(`\nā Stopping due to rate limit/quota exceeded`)
|
|
274
|
-
console.log(`š” Suggestions:`)
|
|
275
|
-
console.log(
|
|
276
|
-
` ⢠Wait for quota to reset (daily limits typically reset at midnight Pacific Time)`,
|
|
277
|
-
)
|
|
278
|
-
console.log(
|
|
279
|
-
` ⢠Switch to a different model: --model claude or --model codex`,
|
|
280
|
-
)
|
|
281
|
-
console.log(` ⢠Resume later with: --resume`)
|
|
282
|
-
} else {
|
|
283
|
-
console.error(
|
|
284
|
-
`\nā Stopping due to failure (after ${LLMService.getMaxRetries()} retry attempts)`,
|
|
285
|
-
)
|
|
286
|
-
console.log(`š” Suggestions:`)
|
|
287
|
-
console.log(` ⢠Check your LLM model configuration and credentials`)
|
|
288
|
-
console.log(
|
|
289
|
-
` ⢠Run with --verbose flag for detailed error information`,
|
|
290
|
-
)
|
|
291
|
-
console.log(` ⢠Resume later with: --resume`)
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
console.log(
|
|
295
|
-
`ā
Successfully analyzed ${analyzedCommits.length} commits before failure`,
|
|
296
|
-
)
|
|
297
|
-
console.log(
|
|
298
|
-
`š Progress saved. Use --resume to continue from commit ${overallIndex + 1}`,
|
|
299
|
-
)
|
|
300
|
-
|
|
301
|
-
// Export what we have so far
|
|
302
|
-
if (analyzedCommits.length > 0) {
|
|
303
|
-
CSVService.exportToFile(analyzedCommits, options.output!)
|
|
304
|
-
console.log(`š Partial results exported to ${options.output}`)
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
process.exit(1)
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (analyzedCommits.length === 0) {
|
|
312
|
-
throw new ValidationError("No commits were successfully analyzed")
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
CSVService.exportToFile(analyzedCommits, options.output!)
|
|
316
|
-
console.log(`\nā
Analysis complete! Results exported to ${options.output}`)
|
|
317
|
-
console.log(
|
|
318
|
-
`Successfully analyzed ${analyzedCommits.length}/${totalCommitsToProcess} commits`,
|
|
319
|
-
)
|
|
320
|
-
|
|
321
|
-
if (failedCommits > 0) {
|
|
322
|
-
console.log(
|
|
323
|
-
`ā ļø Failed to analyze ${failedCommits} commits (see errors above)`,
|
|
324
|
-
)
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Generate report if --report flag is provided
|
|
328
|
-
if (options.report) {
|
|
329
|
-
console.log("\nGenerating condensed markdown report...")
|
|
330
|
-
|
|
331
|
-
// Determine report output filename
|
|
332
|
-
let reportOutput: string
|
|
333
|
-
if (options.output!.endsWith(".csv")) {
|
|
334
|
-
reportOutput = options.output!.replace(".csv", ".md")
|
|
335
|
-
} else {
|
|
336
|
-
reportOutput = options.output! + ".md"
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// Handle default case - if output is commits.csv, make report report.md
|
|
340
|
-
if (reportOutput.endsWith("commits.md")) {
|
|
341
|
-
reportOutput = reportOutput.replace("commits.md", "report.md")
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// If output directory is specified but report output is just a filename, use the output directory
|
|
345
|
-
if (
|
|
346
|
-
options.outputDir &&
|
|
347
|
-
!reportOutput.includes("/") &&
|
|
348
|
-
!reportOutput.includes("\\")
|
|
349
|
-
) {
|
|
350
|
-
reportOutput = CLIService.resolveOutputPath(
|
|
351
|
-
reportOutput.split("/").pop() || reportOutput,
|
|
352
|
-
options.outputDir,
|
|
353
|
-
)
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
try {
|
|
357
|
-
await MarkdownReportGenerator.generateReport(
|
|
358
|
-
options.output!,
|
|
359
|
-
reportOutput,
|
|
360
|
-
)
|
|
361
|
-
console.log(`š Report generated: ${reportOutput}`)
|
|
362
|
-
} catch (error) {
|
|
363
|
-
console.error(
|
|
364
|
-
`ā ļø Failed to generate report: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
365
|
-
)
|
|
366
|
-
console.log(
|
|
367
|
-
"CSV analysis was successful, but report generation failed.",
|
|
368
|
-
)
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// Clear checkpoint on successful completion
|
|
373
|
-
ProgressTracker.clearProgress()
|
|
374
|
-
console.log("ā Progress checkpoint cleared (analysis complete)")
|
|
375
|
-
|
|
376
|
-
const summary = analyzedCommits.reduce(
|
|
377
|
-
(acc, commit) => {
|
|
378
|
-
acc[commit.analysis.category] = (acc[commit.analysis.category] || 0) + 1
|
|
379
|
-
return acc
|
|
380
|
-
},
|
|
381
|
-
{} as Record<string, number>,
|
|
382
|
-
)
|
|
383
|
-
|
|
384
|
-
console.log("\nSummary by category:")
|
|
385
|
-
Object.entries(summary).forEach(([category, count]) => {
|
|
386
|
-
console.log(` ${category}: ${count} commits`)
|
|
387
|
-
})
|
|
388
|
-
} catch (error) {
|
|
389
|
-
handleError(error)
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (require.main === module) {
|
|
394
|
-
main()
|
|
395
|
-
}
|