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.
Files changed (62) hide show
  1. package/.claude/settings.local.json +11 -1
  2. package/README.md +33 -2
  3. package/commits.csv +2 -0
  4. package/eslint.config.mts +45 -0
  5. package/package.json +17 -9
  6. package/src/1.domain/analysis.ts +93 -0
  7. package/src/1.domain/analyzed-commit.ts +97 -0
  8. package/src/1.domain/application-error.ts +32 -0
  9. package/src/1.domain/category.ts +52 -0
  10. package/src/1.domain/commit-analysis-service.ts +92 -0
  11. package/src/1.domain/commit-hash.ts +40 -0
  12. package/src/1.domain/commit.ts +99 -0
  13. package/src/1.domain/date-formatting-service.ts +81 -0
  14. package/src/1.domain/date-range.ts +76 -0
  15. package/src/1.domain/report-generation-service.ts +292 -0
  16. package/src/2.application/analyze-commits.usecase.ts +307 -0
  17. package/src/2.application/generate-report.usecase.ts +204 -0
  18. package/src/2.application/llm-service.ts +54 -0
  19. package/src/2.application/resume-analysis.usecase.ts +123 -0
  20. package/src/3.presentation/analysis-repository.interface.ts +27 -0
  21. package/src/3.presentation/analyze-command.ts +128 -0
  22. package/src/3.presentation/cli-application.ts +255 -0
  23. package/src/3.presentation/command-handler.interface.ts +4 -0
  24. package/src/3.presentation/commit-analysis-controller.ts +101 -0
  25. package/src/3.presentation/commit-repository.interface.ts +47 -0
  26. package/src/3.presentation/console-formatter.ts +129 -0
  27. package/src/3.presentation/progress-repository.interface.ts +49 -0
  28. package/src/3.presentation/report-command.ts +50 -0
  29. package/src/3.presentation/resume-command.ts +59 -0
  30. package/src/3.presentation/storage-repository.interface.ts +33 -0
  31. package/src/3.presentation/storage-service.interface.ts +32 -0
  32. package/src/3.presentation/version-control-service.interface.ts +41 -0
  33. package/src/4.infrastructure/cache-service.ts +271 -0
  34. package/src/4.infrastructure/cached-analysis-repository.ts +46 -0
  35. package/src/4.infrastructure/claude-llm-adapter.ts +124 -0
  36. package/src/4.infrastructure/csv-service.ts +206 -0
  37. package/src/4.infrastructure/file-storage-repository.ts +108 -0
  38. package/src/4.infrastructure/file-system-storage-adapter.ts +87 -0
  39. package/src/4.infrastructure/gemini-llm-adapter.ts +46 -0
  40. package/src/4.infrastructure/git-adapter.ts +116 -0
  41. package/src/4.infrastructure/git-commit-repository.ts +85 -0
  42. package/src/4.infrastructure/json-progress-tracker.ts +182 -0
  43. package/src/4.infrastructure/llm-adapter-factory.ts +26 -0
  44. package/src/4.infrastructure/llm-adapter.ts +455 -0
  45. package/src/4.infrastructure/llm-analysis-repository.ts +38 -0
  46. package/src/4.infrastructure/openai-llm-adapter.ts +57 -0
  47. package/src/di.ts +108 -0
  48. package/src/main.ts +63 -0
  49. package/src/utils/app-paths.ts +36 -0
  50. package/src/utils/concurrency.ts +81 -0
  51. package/src/utils.ts +77 -0
  52. package/tsconfig.json +7 -1
  53. package/src/cli.ts +0 -170
  54. package/src/csv-reader.ts +0 -180
  55. package/src/csv.ts +0 -40
  56. package/src/errors.ts +0 -49
  57. package/src/git.ts +0 -112
  58. package/src/index.ts +0 -395
  59. package/src/llm.ts +0 -396
  60. package/src/progress.ts +0 -84
  61. package/src/report-generator.ts +0 -286
  62. 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
- }