commit-analyzer 1.1.4 → 1.1.6

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 (55) hide show
  1. package/README.md +164 -82
  2. package/dist/main.ts +0 -0
  3. package/package.json +2 -1
  4. package/.claude/settings.local.json +0 -23
  5. package/commits.csv +0 -2
  6. package/csv-to-report-prompt.md +0 -97
  7. package/eslint.config.mts +0 -45
  8. package/prompt.md +0 -69
  9. package/src/1.domain/analysis.ts +0 -93
  10. package/src/1.domain/analyzed-commit.ts +0 -97
  11. package/src/1.domain/application-error.ts +0 -32
  12. package/src/1.domain/category.ts +0 -52
  13. package/src/1.domain/commit-analysis-service.ts +0 -92
  14. package/src/1.domain/commit-hash.ts +0 -40
  15. package/src/1.domain/commit.ts +0 -99
  16. package/src/1.domain/date-formatting-service.ts +0 -81
  17. package/src/1.domain/date-range.ts +0 -76
  18. package/src/1.domain/report-generation-service.ts +0 -443
  19. package/src/2.application/analyze-commits.usecase.ts +0 -307
  20. package/src/2.application/generate-report.usecase.ts +0 -209
  21. package/src/2.application/llm-service.ts +0 -54
  22. package/src/2.application/resume-analysis.usecase.ts +0 -123
  23. package/src/3.presentation/analysis-repository.interface.ts +0 -27
  24. package/src/3.presentation/analyze-command.ts +0 -128
  25. package/src/3.presentation/cli-application.ts +0 -278
  26. package/src/3.presentation/command-handler.interface.ts +0 -4
  27. package/src/3.presentation/commit-analysis-controller.ts +0 -101
  28. package/src/3.presentation/commit-repository.interface.ts +0 -47
  29. package/src/3.presentation/console-formatter.ts +0 -129
  30. package/src/3.presentation/progress-repository.interface.ts +0 -49
  31. package/src/3.presentation/report-command.ts +0 -50
  32. package/src/3.presentation/resume-command.ts +0 -59
  33. package/src/3.presentation/storage-repository.interface.ts +0 -33
  34. package/src/3.presentation/storage-service.interface.ts +0 -32
  35. package/src/3.presentation/version-control-service.interface.ts +0 -46
  36. package/src/4.infrastructure/cache-service.ts +0 -271
  37. package/src/4.infrastructure/cached-analysis-repository.ts +0 -46
  38. package/src/4.infrastructure/claude-llm-adapter.ts +0 -124
  39. package/src/4.infrastructure/csv-service.ts +0 -252
  40. package/src/4.infrastructure/file-storage-repository.ts +0 -108
  41. package/src/4.infrastructure/file-system-storage-adapter.ts +0 -87
  42. package/src/4.infrastructure/gemini-llm-adapter.ts +0 -46
  43. package/src/4.infrastructure/git-adapter.ts +0 -143
  44. package/src/4.infrastructure/git-commit-repository.ts +0 -85
  45. package/src/4.infrastructure/json-progress-tracker.ts +0 -182
  46. package/src/4.infrastructure/llm-adapter-factory.ts +0 -26
  47. package/src/4.infrastructure/llm-adapter.ts +0 -485
  48. package/src/4.infrastructure/llm-analysis-repository.ts +0 -38
  49. package/src/4.infrastructure/openai-llm-adapter.ts +0 -57
  50. package/src/di.ts +0 -109
  51. package/src/main.ts +0 -63
  52. package/src/utils/app-paths.ts +0 -36
  53. package/src/utils/concurrency.ts +0 -81
  54. package/src/utils.ts +0 -77
  55. package/tsconfig.json +0 -25
@@ -1,27 +0,0 @@
1
- import { Analysis } from "@domain/analysis"
2
- import { Commit } from "@domain/commit"
3
-
4
- /**
5
- * Repository interface for commit analysis operations
6
- */
7
- export interface IAnalysisRepository {
8
- /**
9
- * Analyzes a commit and returns the analysis result
10
- */
11
- analyze(commit: Commit): Promise<Analysis>
12
-
13
- /**
14
- * Checks if the analysis service is available
15
- */
16
- isAvailable(): Promise<boolean>
17
-
18
- /**
19
- * Gets the maximum number of retry attempts for analysis
20
- */
21
- getMaxRetries(): number
22
-
23
- /**
24
- * Sets verbose mode for detailed error information
25
- */
26
- setVerbose(verbose: boolean): void
27
- }
@@ -1,128 +0,0 @@
1
- import { AnalyzedCommit } from "@domain/analyzed-commit"
2
- import { CommitAnalysisService } from "@domain/commit-analysis-service"
3
-
4
- import {
5
- AnalyzeCommitsResult,
6
- AnalyzeCommitsUseCase,
7
- } from "@app/analyze-commits.usecase"
8
-
9
- import { getErrorMessage } from "../utils"
10
-
11
- import { ICommitRepository } from "./commit-repository.interface"
12
- import { ConsoleFormatter } from "./console-formatter"
13
-
14
- export interface AnalyzeCommandOptions {
15
- commits: string[]
16
- output: string
17
- author?: string
18
- limit?: number
19
- verbose?: boolean
20
- since?: string
21
- until?: string
22
- batchSize?: number
23
- }
24
-
25
- export class AnalyzeCommand {
26
- constructor(
27
- private readonly analyzeCommitsUseCase: AnalyzeCommitsUseCase,
28
- private readonly commitAnalysisService: CommitAnalysisService,
29
- private readonly commitRepository: ICommitRepository,
30
- ) {}
31
-
32
- async execute(options: AnalyzeCommandOptions): Promise<void> {
33
- try {
34
- const commitHashes = await this.resolveCommitHashes(options)
35
- const analysisResult = await this.performAnalysis(commitHashes, options)
36
- this.displayResults(analysisResult, options.output)
37
- } catch (error) {
38
- ConsoleFormatter.logError(getErrorMessage(error))
39
- throw error
40
- }
41
- }
42
-
43
- private async resolveCommitHashes(
44
- options: AnalyzeCommandOptions,
45
- ): Promise<string[]> {
46
- if (options.commits.length > 0) {
47
- return options.commits
48
- }
49
- const { userEmail, userCommits } = await this.fetchUserCommits(options)
50
- const commitHashes = userCommits.map((commit) =>
51
- commit.getHash().getValue(),
52
- )
53
- if (commitHashes.length === 0) {
54
- ConsoleFormatter.logWarning(`No commits found for user: ${userEmail}`)
55
- throw new Error("No commits found for analysis")
56
- }
57
- ConsoleFormatter.logInfo(
58
- `Found ${commitHashes.length} commits for user: ${userEmail}`,
59
- )
60
- return commitHashes
61
- }
62
-
63
- private async fetchUserCommits(options: AnalyzeCommandOptions) {
64
- if (options.author) {
65
- const userCommits = await this.commitRepository.getByAuthor({
66
- authorEmail: options.author,
67
- limit: options.limit,
68
- since: options.since,
69
- until: options.until,
70
- })
71
- return { userEmail: options.author, userCommits }
72
- }
73
- const userEmail = await this.commitRepository.getCurrentUserEmail()
74
- const userCommits = await this.commitAnalysisService.getCurrentUserCommits({
75
- limit: options.limit,
76
- since: options.since,
77
- until: options.until,
78
- })
79
- return { userEmail, userCommits }
80
- }
81
-
82
- private async performAnalysis(
83
- commitHashes: string[],
84
- options: AnalyzeCommandOptions,
85
- ): Promise<AnalyzeCommitsResult> {
86
- if (commitHashes.length === 0) {
87
- throw new Error("No commits provided for analysis")
88
- }
89
- return await this.analyzeCommitsUseCase.handle({
90
- commitHashes,
91
- outputFile: options.output,
92
- verbose: options.verbose,
93
- batchSize: options.batchSize,
94
- })
95
- }
96
-
97
- private displayResults(
98
- result: AnalyzeCommitsResult,
99
- outputFile: string,
100
- ): void {
101
- ConsoleFormatter.logSection(
102
- `Analysis complete! Results exported to ${outputFile}`,
103
- )
104
- ConsoleFormatter.logSuccess(
105
- `Successfully analyzed ${result.analyzedCommits.length}/${result.totalProcessed} commits`,
106
- )
107
- if (result.failedCommits > 0) {
108
- ConsoleFormatter.logWarning(
109
- `Failed to analyze ${result.failedCommits} commits (see errors above)`,
110
- )
111
- }
112
- const summary = this.createAnalysisSummary(result.analyzedCommits)
113
- ConsoleFormatter.displayAnalysisSummary(summary)
114
- }
115
-
116
- private createAnalysisSummary(
117
- analyzedCommits: AnalyzedCommit[],
118
- ): Record<string, number> {
119
- return analyzedCommits.reduce(
120
- (summary, commit) => {
121
- const category = commit.getAnalysis().getCategory().getValue()
122
- summary[category] = (summary[category] || 0) + 1
123
- return summary
124
- },
125
- {} as Record<string, number>,
126
- )
127
- }
128
- }
@@ -1,278 +0,0 @@
1
- import { Command } from "commander"
2
-
3
- import { getErrorMessage } from "../utils"
4
-
5
- import { CommitAnalysisController } from "./commit-analysis-controller"
6
- import { ConsoleFormatter } from "./console-formatter"
7
-
8
- export interface CLIOptions {
9
- output?: string
10
- outputDir?: string
11
- commits: string[]
12
- author?: string
13
- limit?: number
14
- resume?: boolean
15
- clear?: boolean
16
- llm?: string
17
- report?: boolean
18
- inputCsv?: string
19
- verbose?: boolean
20
- since?: string
21
- until?: string
22
- noCache?: boolean
23
- batchSize?: number
24
- }
25
-
26
- export class CLIApplication {
27
- private static readonly VERSION = "1.1.4"
28
- private static readonly DEFAULT_COMMITS_OUTPUT_FILE = "results/commits.csv"
29
- private static readonly DEFAULT_REPORT_OUTPUT_FILE = "results/report.md"
30
-
31
- constructor(private readonly controller: CommitAnalysisController) {}
32
-
33
- async run(args: string[]): Promise<void> {
34
- try {
35
- const program = this.createProgram()
36
- await program.parseAsync(args)
37
- } catch (error) {
38
- ConsoleFormatter.logError(getErrorMessage(error))
39
- process.exit(1)
40
- }
41
- }
42
-
43
- private createProgram(): Command {
44
- const program = new Command()
45
-
46
- program
47
- .name("commit-analyzer")
48
- .description(
49
- "Analyze user authored git commits and generate rich commit descriptions and stakeholder reports from them.",
50
- )
51
- .version(CLIApplication.VERSION)
52
- .option(
53
- "-o, --output <file>",
54
- `Output CSV file (default: ${CLIApplication.DEFAULT_COMMITS_OUTPUT_FILE})`,
55
- )
56
- .option(
57
- "--output-dir <dir>",
58
- "Output directory for CSV and report files (default: current directory)",
59
- )
60
- .option(
61
- "-a, --author <email>",
62
- "Filter commits by author email (defaults to current user)",
63
- )
64
- .option(
65
- "-l, --limit <number>",
66
- "Limit number of commits to analyze",
67
- parseInt,
68
- )
69
- .option("-r, --resume", "Resume from last checkpoint if available")
70
- .option("-c, --clear", "Clear any existing progress checkpoint")
71
- .option("--llm <llm>", "LLM CLI tool to use (claude, gemini, openai)")
72
- .option(
73
- "--report",
74
- "Generate condensed markdown report from existing CSV",
75
- )
76
- .option(
77
- "--input-csv <file>",
78
- "Input CSV file to read for report generation",
79
- )
80
- .option(
81
- "-v, --verbose",
82
- "Enable verbose logging (shows detailed error information)",
83
- )
84
- .option(
85
- "--since <date>",
86
- "Only analyze commits since this date (YYYY-MM-DD, '1 week ago', '2024-01-01')",
87
- )
88
- .option(
89
- "--until <date>",
90
- "Only analyze commits until this date (YYYY-MM-DD, '1 day ago', '2024-12-31')",
91
- )
92
- .option("--no-cache", "Disable caching of analysis results")
93
- .option(
94
- "--batch-size <number>",
95
- "Number of commits to process per batch (default: 1 for sequential processing)",
96
- parseInt,
97
- )
98
- .argument(
99
- "[commits...]",
100
- "Commit hashes to analyze (if none provided, uses current user's commits)",
101
- )
102
- .action(async (commits: string[], options: Record<string, unknown>) => {
103
- const cliOptions = this.parseOptions(options, commits)
104
- await this.executeCommand(cliOptions)
105
- })
106
- return program
107
- }
108
-
109
- private parseOptions(
110
- options: Record<string, unknown>,
111
- args: string[],
112
- ): CLIOptions {
113
- const { commits } = this.determineCommitsToAnalyze(args)
114
- return {
115
- output: this.determineOutputPath(
116
- this.getStringOption(options.output),
117
- this.getStringOption(options.outputDir),
118
- ),
119
- outputDir: this.getStringOption(options.outputDir),
120
- commits,
121
- author: this.getStringOption(options.author),
122
- limit: this.getNumberOption(options.limit),
123
- resume: this.getBooleanOption(options.resume),
124
- clear: this.getBooleanOption(options.clear),
125
- llm: this.getStringOption(options.llm),
126
- report: this.getBooleanOption(options.report),
127
- inputCsv: this.getStringOption(options.inputCsv),
128
- verbose: this.getBooleanOption(options.verbose),
129
- since: this.getStringOption(options.since),
130
- until: this.getStringOption(options.until),
131
- noCache: this.getBooleanOption(options.noCache),
132
- batchSize: this.getNumberOption(options.batchSize),
133
- }
134
- }
135
-
136
- private getStringOption(value: unknown): string | undefined {
137
- return typeof value === "string" ? value : undefined
138
- }
139
-
140
- private getNumberOption(value: unknown): number | undefined {
141
- return typeof value === "number" ? value : undefined
142
- }
143
-
144
- private getBooleanOption(value: unknown): boolean | undefined {
145
- return typeof value === "boolean" ? value : undefined
146
- }
147
-
148
- private async executeCommand(options: CLIOptions): Promise<void> {
149
- // Handle clear flag
150
- if (options.clear) {
151
- await this.controller.handleClearProgress()
152
- if (!options.resume) {
153
- return
154
- }
155
- }
156
-
157
- // Handle input CSV mode (report generation only)
158
- if (options.inputCsv) {
159
- const reportOutputPath = this.determineReportOutputPath(options)
160
-
161
- await this.controller.handleReportGeneration({
162
- inputCsv: options.inputCsv,
163
- output: reportOutputPath,
164
- sourceInfo: { type: "csv", value: options.inputCsv },
165
- })
166
- return
167
- }
168
-
169
- // Handle resume mode
170
- if (options.resume) {
171
- const resumed = await this.controller.handleResumeAnalysis({
172
- verbose: options.verbose,
173
- })
174
-
175
- if (resumed && options.report) {
176
- const reportOutput = options.output
177
- ? this.getReportOutputPath(options.output)
178
- : CLIApplication.DEFAULT_REPORT_OUTPUT_FILE
179
-
180
- await this.controller.handleReportGeneration({
181
- inputCsv: options.output,
182
- output: reportOutput,
183
- sourceInfo: {
184
- type: "csv",
185
- value: options.output || CLIApplication.DEFAULT_COMMITS_OUTPUT_FILE,
186
- },
187
- })
188
- }
189
- return
190
- }
191
-
192
- // Handle normal analysis mode
193
- const analyzeOptions = {
194
- commits: options.commits,
195
- output: options.output!,
196
- author: options.author,
197
- limit: options.limit,
198
- verbose: options.verbose,
199
- since: options.since,
200
- until: options.until,
201
- batchSize: options.batchSize,
202
- }
203
-
204
- if (options.report) {
205
- // Analysis + Report workflow
206
- const reportOutput = options.output
207
- ? this.getReportOutputPath(options.output)
208
- : CLIApplication.DEFAULT_REPORT_OUTPUT_FILE
209
-
210
- await this.controller.handleAnalysisWithReport(analyzeOptions, {
211
- output: reportOutput,
212
- sourceInfo:
213
- options.commits.length > 0
214
- ? { type: "commits", value: options.commits.join(",") }
215
- : { type: "author", value: options.author || "current user" },
216
- })
217
- } else {
218
- // Analysis only workflow
219
- await this.controller.handleAnalysis(analyzeOptions)
220
- }
221
- }
222
-
223
- private determineCommitsToAnalyze(args: string[]): { commits: string[] } {
224
- let commits: string[] = []
225
- if (args.length > 0) {
226
- commits = args
227
- }
228
- return { commits }
229
- }
230
-
231
- private determineOutputPath(
232
- outputOption?: string,
233
- outputDir?: string,
234
- ): string {
235
- if (outputOption) {
236
- return outputOption
237
- }
238
- if (outputDir) {
239
- return `${outputDir}/commits.csv`
240
- }
241
- return CLIApplication.DEFAULT_COMMITS_OUTPUT_FILE
242
- }
243
-
244
- private getReportOutputPath(csvPath: string): string {
245
- // If no specific output path provided, use the default report path
246
- if (!csvPath) {
247
- return CLIApplication.DEFAULT_REPORT_OUTPUT_FILE
248
- }
249
-
250
- // Extract directory from CSV path and use report.md as filename
251
- if (csvPath.endsWith(".csv")) {
252
- const dir = csvPath.substring(0, csvPath.lastIndexOf("/"))
253
- return dir ? `${dir}/report.md` : "report.md"
254
- }
255
- return csvPath + ".md"
256
- }
257
-
258
- private determineReportOutputPath(options: CLIOptions): string {
259
- // Check if explicit output or outputDir was provided in raw options
260
- // We need to check raw options to distinguish between user-provided and default values
261
- const hasExplicitOutput =
262
- process.argv.includes("--output") || process.argv.includes("-o")
263
- const hasExplicitOutputDir = process.argv.includes("--output-dir")
264
-
265
- if (hasExplicitOutput || hasExplicitOutputDir) {
266
- return options.output || CLIApplication.DEFAULT_REPORT_OUTPUT_FILE
267
- }
268
-
269
- // Default: generate report in same directory as CSV with .md extension
270
- return this.getReportPathFromCsv(options.inputCsv!)
271
- }
272
-
273
- private getReportPathFromCsv(csvPath: string): string {
274
- const lastSlash = csvPath.lastIndexOf("/")
275
- const directory = lastSlash >= 0 ? csvPath.substring(0, lastSlash + 1) : ""
276
- return directory + "report.md"
277
- }
278
- }
@@ -1,4 +0,0 @@
1
- export interface ICommandHandler<TCommand, TResult> {
2
- handle(command: TCommand): Promise<TResult>
3
- }
4
-
@@ -1,101 +0,0 @@
1
- import { CacheService } from "@infra/cache-service"
2
-
3
- import { AnalyzeCommand, AnalyzeCommandOptions } from "./analyze-command"
4
- import { ICommitRepository } from "./commit-repository.interface"
5
- import { ConsoleFormatter } from "./console-formatter"
6
- import { IProgressRepository } from "./progress-repository.interface"
7
- import { ReportCommand, ReportCommandOptions } from "./report-command"
8
- import { ResumeCommand, ResumeCommandOptions } from "./resume-command"
9
-
10
- export class CommitAnalysisController {
11
- constructor(
12
- private readonly analyzeCommand: AnalyzeCommand,
13
- private readonly reportCommand: ReportCommand,
14
- private readonly resumeCommand: ResumeCommand,
15
- private readonly progressRepository: IProgressRepository,
16
- private readonly cacheService: CacheService,
17
- private readonly commitRepository: ICommitRepository,
18
- ) {}
19
-
20
- /**
21
- * Handles the main analysis workflow
22
- */
23
- async handleAnalysis(options: AnalyzeCommandOptions): Promise<void> {
24
- await this.analyzeCommand.execute(options)
25
- }
26
-
27
- /**
28
- * Handles report generation
29
- */
30
- async handleReportGeneration(options: ReportCommandOptions): Promise<void> {
31
- await this.reportCommand.execute(options)
32
- }
33
-
34
- /**
35
- * Handles resuming analysis from checkpoint
36
- */
37
- async handleResumeAnalysis(options: ResumeCommandOptions): Promise<boolean> {
38
- return this.resumeCommand.execute(options)
39
- }
40
-
41
- /**
42
- * Handles the combined analysis and report workflow
43
- */
44
- async handleAnalysisWithReport(
45
- analyzeOptions: AnalyzeCommandOptions,
46
- reportOptions: ReportCommandOptions,
47
- ): Promise<void> {
48
- // First run analysis
49
- await this.handleAnalysis(analyzeOptions)
50
-
51
- // Resolve source info with actual user email if needed
52
- const resolvedSourceInfo = await this.resolveSourceInfo(
53
- reportOptions.sourceInfo,
54
- analyzeOptions.author,
55
- )
56
-
57
- // Then generate report using the analysis output
58
- const reportOptionsWithInput: ReportCommandOptions = {
59
- ...reportOptions,
60
- inputCsv: analyzeOptions.output,
61
- sourceInfo: resolvedSourceInfo,
62
- }
63
-
64
- await this.handleReportGeneration(reportOptionsWithInput)
65
- }
66
-
67
- /**
68
- * Handles clearing progress and cache
69
- */
70
- async handleClearProgress(): Promise<void> {
71
- await this.progressRepository.clearProgress()
72
- await this.cacheService.clear()
73
- ConsoleFormatter.logSuccess("✓ Progress checkpoint and cache cleared")
74
- }
75
-
76
- /**
77
- * Resolves source info with actual user email when needed
78
- */
79
- private async resolveSourceInfo(
80
- sourceInfo?: { type: "author" | "commits" | "csv"; value: string },
81
- authorOption?: string,
82
- ): Promise<
83
- { type: "author" | "commits" | "csv"; value: string } | undefined
84
- > {
85
- if (!sourceInfo) {
86
- return undefined
87
- }
88
-
89
- // If source is author and value is 'current user', get actual email
90
- if (sourceInfo.type === "author" && sourceInfo.value === "current user") {
91
- const actualEmail =
92
- authorOption || (await this.commitRepository.getCurrentUserEmail())
93
- return {
94
- type: "author",
95
- value: actualEmail,
96
- }
97
- }
98
-
99
- return sourceInfo
100
- }
101
- }
@@ -1,47 +0,0 @@
1
- import { Commit } from "@domain/commit"
2
- import { CommitHash } from "@domain/commit-hash"
3
-
4
- /**
5
- * Repository interface for accessing commit data
6
- */
7
- export interface ICommitRepository {
8
- /**
9
- * Retrieves a commit by its hash
10
- */
11
- getByHash(hash: CommitHash): Promise<Commit>
12
-
13
- /**
14
- * Retrieves commits authored by a specific user
15
- */
16
- getByAuthor(params: {
17
- authorEmail: string
18
- limit?: number
19
- since?: string
20
- until?: string
21
- }): Promise<Commit[]>
22
-
23
- /**
24
- * Retrieves commits from a list of hashes
25
- */
26
- getByHashes(hashes: CommitHash[]): Promise<Commit[]>
27
-
28
- /**
29
- * Validates if a commit hash exists in the repository
30
- */
31
- exists(hash: CommitHash): Promise<boolean>
32
-
33
- /**
34
- * Gets the current user's email from the repository configuration
35
- */
36
- getCurrentUserEmail(): Promise<string>
37
-
38
- /**
39
- * Gets the current user's name from the repository configuration
40
- */
41
- getCurrentUserName(): Promise<string>
42
-
43
- /**
44
- * Checks if the current directory is a valid repository
45
- */
46
- isValidRepository(): Promise<boolean>
47
- }