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
@@ -0,0 +1,27 @@
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
+ }
@@ -0,0 +1,128 @@
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
+ }
@@ -0,0 +1,255 @@
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.0"
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
+ await this.controller.handleReportGeneration({
160
+ inputCsv: options.inputCsv,
161
+ output: options.output || CLIApplication.DEFAULT_REPORT_OUTPUT_FILE,
162
+ sourceInfo: { type: "csv", value: options.inputCsv },
163
+ })
164
+ return
165
+ }
166
+
167
+ // Handle resume mode
168
+ if (options.resume) {
169
+ const resumed = await this.controller.handleResumeAnalysis({
170
+ verbose: options.verbose,
171
+ })
172
+
173
+ if (resumed && options.report) {
174
+ const reportOutput = options.output
175
+ ? this.getReportOutputPath(options.output)
176
+ : CLIApplication.DEFAULT_REPORT_OUTPUT_FILE
177
+
178
+ await this.controller.handleReportGeneration({
179
+ inputCsv: options.output,
180
+ output: reportOutput,
181
+ sourceInfo: {
182
+ type: "csv",
183
+ value: options.output || CLIApplication.DEFAULT_COMMITS_OUTPUT_FILE,
184
+ },
185
+ })
186
+ }
187
+ return
188
+ }
189
+
190
+ // Handle normal analysis mode
191
+ const analyzeOptions = {
192
+ commits: options.commits,
193
+ output: options.output!,
194
+ author: options.author,
195
+ limit: options.limit,
196
+ verbose: options.verbose,
197
+ since: options.since,
198
+ until: options.until,
199
+ batchSize: options.batchSize,
200
+ }
201
+
202
+ if (options.report) {
203
+ // Analysis + Report workflow
204
+ const reportOutput = options.output
205
+ ? this.getReportOutputPath(options.output)
206
+ : CLIApplication.DEFAULT_REPORT_OUTPUT_FILE
207
+
208
+ await this.controller.handleAnalysisWithReport(analyzeOptions, {
209
+ output: reportOutput,
210
+ sourceInfo:
211
+ options.commits.length > 0
212
+ ? { type: "commits", value: options.commits.join(",") }
213
+ : { type: "author", value: options.author || "current user" },
214
+ })
215
+ } else {
216
+ // Analysis only workflow
217
+ await this.controller.handleAnalysis(analyzeOptions)
218
+ }
219
+ }
220
+
221
+ private determineCommitsToAnalyze(args: string[]): { commits: string[] } {
222
+ let commits: string[] = []
223
+ if (args.length > 0) {
224
+ commits = args
225
+ }
226
+ return { commits }
227
+ }
228
+
229
+ private determineOutputPath(
230
+ outputOption?: string,
231
+ outputDir?: string,
232
+ ): string {
233
+ if (outputOption) {
234
+ return outputOption
235
+ }
236
+ if (outputDir) {
237
+ return `${outputDir}/commits.csv`
238
+ }
239
+ return CLIApplication.DEFAULT_COMMITS_OUTPUT_FILE
240
+ }
241
+
242
+ private getReportOutputPath(csvPath: string): string {
243
+ // If no specific output path provided, use the default report path
244
+ if (!csvPath) {
245
+ return CLIApplication.DEFAULT_REPORT_OUTPUT_FILE
246
+ }
247
+
248
+ // Extract directory from CSV path and use report.md as filename
249
+ if (csvPath.endsWith(".csv")) {
250
+ const dir = csvPath.substring(0, csvPath.lastIndexOf("/"))
251
+ return dir ? `${dir}/report.md` : "report.md"
252
+ }
253
+ return csvPath + ".md"
254
+ }
255
+ }
@@ -0,0 +1,4 @@
1
+ export interface ICommandHandler<TCommand, TResult> {
2
+ handle(command: TCommand): Promise<TResult>
3
+ }
4
+
@@ -0,0 +1,101 @@
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
+ }
@@ -0,0 +1,47 @@
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
+ }
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Console formatter for consistent output styling
3
+ */
4
+ export class ConsoleFormatter {
5
+ /**
6
+ * Log a success message with checkmark
7
+ */
8
+ static logSuccess(message: string): void {
9
+ console.log(`✓ ${message}`)
10
+ }
11
+
12
+ /**
13
+ * Log an error message with X mark
14
+ */
15
+ static logError(message: string): void {
16
+ console.error(`❌ ${message}`)
17
+ }
18
+
19
+ /**
20
+ * Log a warning message with warning icon
21
+ */
22
+ static logWarning(message: string): void {
23
+ console.log(`⚠️ ${message}`)
24
+ }
25
+
26
+ /**
27
+ * Log a debug message for development purposes
28
+ */
29
+ static logDebug(message: string): void {
30
+ console.log(`🐛 ${message}`)
31
+ }
32
+
33
+ /**
34
+ * Log an info message with bullet point
35
+ */
36
+ static logInfo(message: string): void {
37
+ console.log(` - ${message}`)
38
+ }
39
+
40
+ /**
41
+ * Log a progress message with arrow
42
+ */
43
+ static logProgress(message: string): void {
44
+ console.log(`▶️ ${message}`)
45
+ }
46
+
47
+ /**
48
+ * Log a completion message with celebration icon
49
+ */
50
+ static logComplete(message: string): void {
51
+ console.log(`🎉 ${message}`)
52
+ }
53
+
54
+ /**
55
+ * Log a file operation with folder icon
56
+ */
57
+ static logFile(message: string): void {
58
+ console.log(`📁 ${message}`)
59
+ }
60
+
61
+ /**
62
+ * Log a save operation with disk icon
63
+ */
64
+ static logSave(message: string): void {
65
+ console.log(`💾 ${message}`)
66
+ }
67
+
68
+ /**
69
+ * Log a report generation with chart icon
70
+ */
71
+ static logReport(message: string): void {
72
+ console.log(`📊 ${message}`)
73
+ }
74
+
75
+ /**
76
+ * Log with indentation for nested information
77
+ */
78
+ static logIndented(message: string, level: number = 1): void {
79
+ const indent = ' '.repeat(level)
80
+ console.log(`${indent}${message}`)
81
+ }
82
+
83
+ /**
84
+ * Log a section header with newlines for spacing
85
+ */
86
+ static logSection(title: string): void {
87
+ console.log(`\n${title}`)
88
+ }
89
+
90
+ /**
91
+ * Log with custom emoji/icon
92
+ */
93
+ static logWithIcon(icon: string, message: string): void {
94
+ console.log(`${icon} ${message}`)
95
+ }
96
+
97
+ /**
98
+ * Display analysis summary in a formatted way
99
+ */
100
+ static displayAnalysisSummary(summary: Record<string, number>): void {
101
+ this.logSection("Summary by category:")
102
+ Object.entries(summary).forEach(([category, count]) => {
103
+ this.logInfo(`${category}: ${count} commits`)
104
+ })
105
+ }
106
+
107
+ /**
108
+ * Update progress bar inline
109
+ */
110
+ static updateProgress(current: number, total: number, description?: string): void {
111
+ const percentage = Math.round((current / total) * 100)
112
+ const completed = Math.round((current / total) * 20) // 20 characters for progress bar
113
+ const remaining = 20 - completed
114
+
115
+ const progressBar = '█'.repeat(completed) + '░'.repeat(remaining)
116
+ const desc = description ? ` ${description}` : ''
117
+
118
+ // Use \r to overwrite the line
119
+ process.stdout.write(`\r▶️ [${progressBar}] ${percentage}% (${current}/${total})${desc}`)
120
+ }
121
+
122
+ /**
123
+ * Complete progress and clear the line
124
+ */
125
+ static completeProgress(): void {
126
+ // Clear the current line and move to beginning
127
+ process.stdout.write('\r\x1b[K')
128
+ }
129
+ }