commit-analyzer 1.1.0 → 1.1.2

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.
@@ -14,7 +14,8 @@
14
14
  "Bash(git checkout:*)",
15
15
  "Bash(npx ts-node:*)",
16
16
  "Bash(node:*)",
17
- "Bash(npm start:*)"
17
+ "Bash(npm start:*)",
18
+ "Bash(bun:*)"
18
19
  ],
19
20
  "deny": [],
20
21
  "ask": []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "commit-analyzer",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Analyze git commits and generate categories, summaries, and descriptions for each commit. Optionally generate a yearly breakdown report of your commit history.",
5
5
  "main": "dist/main.ts",
6
6
  "bin": {
@@ -16,7 +16,8 @@
16
16
  "lint": "eslint src/**/*.ts",
17
17
  "typecheck": "tsc --noEmit",
18
18
  "link": "bun link",
19
- "publish": "bun publish"
19
+ "publish": "bun publish",
20
+ "deploy": "bun run build && bun link && bun publish"
20
21
  },
21
22
  "keywords": [
22
23
  "git",
@@ -7,6 +7,7 @@ import {
7
7
 
8
8
  import { ICommandHandler } from "@presentation/command-handler.interface"
9
9
  import { IStorageRepository } from "@presentation/storage-repository.interface"
10
+ import { IVersionControlService } from "@presentation/version-control-service.interface"
10
11
 
11
12
  import { ILLMService } from "./llm-service"
12
13
 
@@ -35,6 +36,7 @@ export class GenerateReportUseCase
35
36
  private readonly storageRepository: IStorageRepository,
36
37
  private readonly llmService: ILLMService,
37
38
  private readonly dateFormattingService: DateFormattingService,
39
+ private readonly versionControlService: IVersionControlService,
38
40
  ) {}
39
41
 
40
42
  async handle(command: GenerateReportCommand): Promise<GenerateReportResult> {
@@ -99,7 +101,10 @@ export class GenerateReportUseCase
99
101
  sourceInfo?: { type: 'author' | 'commits' | 'csv'; value: string }
100
102
  }): Promise<void> {
101
103
  const { commits, statistics, outputPath, includeStatistics, sourceInfo } = params
102
- let reportContent = "# Development Summary Report\n\n"
104
+
105
+ // Get repository name for the report heading
106
+ const repositoryName = await this.versionControlService.getRepositoryName()
107
+ let reportContent = `# Development Report for ${repositoryName}\n\n`
103
108
 
104
109
  if (includeStatistics) {
105
110
  reportContent += this.generateAnalysisSection(commits, statistics, sourceInfo)
@@ -24,7 +24,7 @@ export interface CLIOptions {
24
24
  }
25
25
 
26
26
  export class CLIApplication {
27
- private static readonly VERSION = "1.1.0"
27
+ private static readonly VERSION = "1.1.2"
28
28
  private static readonly DEFAULT_COMMITS_OUTPUT_FILE = "results/commits.csv"
29
29
  private static readonly DEFAULT_REPORT_OUTPUT_FILE = "results/report.md"
30
30
 
@@ -156,9 +156,11 @@ export class CLIApplication {
156
156
 
157
157
  // Handle input CSV mode (report generation only)
158
158
  if (options.inputCsv) {
159
+ const reportOutputPath = this.determineReportOutputPath(options)
160
+
159
161
  await this.controller.handleReportGeneration({
160
162
  inputCsv: options.inputCsv,
161
- output: options.output || CLIApplication.DEFAULT_REPORT_OUTPUT_FILE,
163
+ output: reportOutputPath,
162
164
  sourceInfo: { type: "csv", value: options.inputCsv },
163
165
  })
164
166
  return
@@ -252,4 +254,26 @@ export class CLIApplication {
252
254
  }
253
255
  return csvPath + ".md"
254
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
+ if (csvPath.endsWith(".csv")) {
275
+ return csvPath.replace(/\.csv$/, ".md")
276
+ }
277
+ return csvPath + ".md"
278
+ }
255
279
  }
@@ -29,6 +29,11 @@ export interface IVersionControlService {
29
29
  */
30
30
  getCurrentUserName(): Promise<string>
31
31
 
32
+ /**
33
+ * Gets the repository/project name
34
+ */
35
+ getRepositoryName(): Promise<string>
36
+
32
37
  /**
33
38
  * Gets commits authored by a specific user
34
39
  */
@@ -64,15 +64,61 @@ export class CSVService {
64
64
  return `"${field.replace(/"/g, '""')}"`
65
65
  }
66
66
 
67
+ /**
68
+ * Split CSV content into rows, properly handling quoted fields that may contain newlines
69
+ */
70
+ private splitCSVIntoRows(content: string): string[] {
71
+ const rows: string[] = []
72
+ let currentRow = ""
73
+ let inQuotes = false
74
+ let i = 0
75
+
76
+ while (i < content.length) {
77
+ const char = content[i]
78
+ const nextChar = content[i + 1]
79
+
80
+ if (char === '"') {
81
+ if (inQuotes && nextChar === '"') {
82
+ // Escaped quote inside quoted field
83
+ currentRow += '""'
84
+ i += 2
85
+ } else {
86
+ // Start or end of quoted field
87
+ inQuotes = !inQuotes
88
+ currentRow += '"'
89
+ i++
90
+ }
91
+ } else if (char === "\n" && !inQuotes) {
92
+ // Row separator outside quotes
93
+ if (currentRow.trim().length > 0) {
94
+ rows.push(currentRow)
95
+ }
96
+ currentRow = ""
97
+ i++
98
+ } else {
99
+ // Regular character (including newlines inside quotes)
100
+ currentRow += char
101
+ i++
102
+ }
103
+ }
104
+
105
+ // Add the last row if it exists
106
+ if (currentRow.trim().length > 0) {
107
+ rows.push(currentRow)
108
+ }
109
+
110
+ return rows
111
+ }
112
+
67
113
  private parseCSV(content: string): AnalyzedCommit[] {
68
- const lines = content.split("\n").filter((line) => line.trim().length > 0)
114
+ const rows = this.splitCSVIntoRows(content)
69
115
 
70
- if (lines.length < 2) {
116
+ if (rows.length < 2) {
71
117
  throw new Error("Invalid CSV format: no data rows found")
72
118
  }
73
119
 
74
120
  // Validate header
75
- const header = lines[0].toLowerCase()
121
+ const header = rows[0].toLowerCase()
76
122
  const expectedHeader = "timestamp,category,summary,description"
77
123
  if (header !== expectedHeader) {
78
124
  throw new Error(
@@ -81,7 +127,7 @@ export class CSVService {
81
127
  }
82
128
 
83
129
  // Skip header row
84
- const dataRows = lines.slice(1)
130
+ const dataRows = rows.slice(1)
85
131
  const commits: AnalyzedCommit[] = []
86
132
 
87
133
  for (let i = 0; i < dataRows.length; i++) {
@@ -82,6 +82,33 @@ export class GitAdapter implements IVersionControlService {
82
82
  }
83
83
  }
84
84
 
85
+ async getRepositoryName(): Promise<string> {
86
+ try {
87
+ // Try to get the repository name from remote origin URL
88
+ const remoteUrl = execSync("git config --get remote.origin.url", GitAdapter.EXEC_OPTIONS).trim()
89
+
90
+ // Extract repository name from various URL formats
91
+ // git@github.com:user/repo.git -> repo
92
+ // https://github.com/user/repo.git -> repo
93
+ // https://github.com/user/repo -> repo
94
+ const match = remoteUrl.match(/\/([^\/]+?)(?:\.git)?$/)
95
+ if (match && match[1]) {
96
+ return match[1]
97
+ }
98
+
99
+ // Fallback: get the directory name
100
+ const dirName = execSync("basename $(git rev-parse --show-toplevel)", GitAdapter.EXEC_OPTIONS).trim()
101
+ return dirName
102
+ } catch (error) {
103
+ // Final fallback: use current directory name
104
+ try {
105
+ return execSync("basename $(pwd)", GitAdapter.EXEC_OPTIONS).trim()
106
+ } catch {
107
+ return "Unknown Project"
108
+ }
109
+ }
110
+ }
111
+
85
112
  async getUserAuthoredCommits(params: {
86
113
  authorEmail: string
87
114
  limit?: number
package/src/di.ts CHANGED
@@ -74,6 +74,7 @@ export class DIContainer {
74
74
  this.storageRepository,
75
75
  this.llmAdapter,
76
76
  this.dateFormattingService,
77
+ this.gitAdapter,
77
78
  )
78
79
 
79
80
  private readonly resumeAnalysisUseCase = new ResumeAnalysisUseCase(