commit-analyzer 1.0.3 → 1.1.1
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 +18 -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 +209 -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 +46 -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 +252 -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 +143 -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 +109 -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 -411
- package/src/progress.ts +0 -84
- package/src/report-generator.ts +0 -286
- package/src/types.ts +0 -24
package/src/main.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { ApplicationError } from "@domain/application-error"
|
|
4
|
+
|
|
5
|
+
import { ConsoleFormatter } from "@presentation/console-formatter"
|
|
6
|
+
|
|
7
|
+
import { DIContainer } from "./di"
|
|
8
|
+
|
|
9
|
+
async function main(): Promise<void> {
|
|
10
|
+
try {
|
|
11
|
+
// Extract options from command line args before creating container
|
|
12
|
+
const llmOption = extractLLMOption(process.argv)
|
|
13
|
+
const noCacheOption = extractNoCacheOption(process.argv)
|
|
14
|
+
const container = new DIContainer({
|
|
15
|
+
llm: llmOption,
|
|
16
|
+
noCache: noCacheOption
|
|
17
|
+
})
|
|
18
|
+
const app = container.getApplication()
|
|
19
|
+
|
|
20
|
+
await app.run(process.argv)
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (error instanceof ApplicationError) {
|
|
23
|
+
ConsoleFormatter.logError(`[${error.code}]: ${error.message}`)
|
|
24
|
+
process.exit(1)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (error instanceof Error) {
|
|
28
|
+
ConsoleFormatter.logError(`Unexpected error: ${error.message}`)
|
|
29
|
+
process.exit(1)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
ConsoleFormatter.logError("Unknown error occurred")
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extract the --llm option from command line arguments
|
|
39
|
+
* This is needed before creating the DI container
|
|
40
|
+
*/
|
|
41
|
+
function extractLLMOption(args: string[]): string | undefined {
|
|
42
|
+
const llmIndex = args.findIndex(arg => arg === '--llm')
|
|
43
|
+
if (llmIndex !== -1 && llmIndex + 1 < args.length) {
|
|
44
|
+
return args[llmIndex + 1]
|
|
45
|
+
}
|
|
46
|
+
return undefined
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extract the --no-cache option from command line arguments
|
|
51
|
+
* This is needed before creating the DI container
|
|
52
|
+
*/
|
|
53
|
+
function extractNoCacheOption(args: string[]): boolean {
|
|
54
|
+
return args.includes('--no-cache')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Run the application if this file is executed directly
|
|
58
|
+
if (require.main === module) {
|
|
59
|
+
main().catch((error) => {
|
|
60
|
+
ConsoleFormatter.logError(`Failed to bootstrap application: ${error}`)
|
|
61
|
+
process.exit(1)
|
|
62
|
+
})
|
|
63
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Utility for managing application data directory paths
|
|
5
|
+
*/
|
|
6
|
+
export class AppPaths {
|
|
7
|
+
private static readonly APP_DATA_DIR = ".commit-analyzer"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Get the application data directory path
|
|
11
|
+
*/
|
|
12
|
+
static getAppDataDir(baseDir: string = process.cwd()): string {
|
|
13
|
+
return path.join(baseDir, AppPaths.APP_DATA_DIR)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get the cache directory path
|
|
18
|
+
*/
|
|
19
|
+
static getCacheDir(baseDir: string = process.cwd()): string {
|
|
20
|
+
return path.join(AppPaths.getAppDataDir(baseDir), "cache")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get the progress file path
|
|
25
|
+
*/
|
|
26
|
+
static getProgressFilePath(baseDir: string = process.cwd()): string {
|
|
27
|
+
return path.join(AppPaths.getAppDataDir(baseDir), "progress.json")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get any file path within the app data directory
|
|
32
|
+
*/
|
|
33
|
+
static getAppDataFilePath(fileName: string, baseDir: string = process.cwd()): string {
|
|
34
|
+
return path.join(AppPaths.getAppDataDir(baseDir), fileName)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for managing concurrent operations with semaphore-like behavior
|
|
3
|
+
*/
|
|
4
|
+
export class ConcurrencyManager {
|
|
5
|
+
private running = 0
|
|
6
|
+
private queue: (() => void)[] = []
|
|
7
|
+
|
|
8
|
+
constructor(private readonly maxConcurrency: number) {}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Execute a function with concurrency control
|
|
12
|
+
*/
|
|
13
|
+
async execute<T>(fn: () => Promise<T>): Promise<T> {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const runTask = async () => {
|
|
16
|
+
this.running++
|
|
17
|
+
try {
|
|
18
|
+
const result = await fn()
|
|
19
|
+
resolve(result)
|
|
20
|
+
} catch (error) {
|
|
21
|
+
reject(error)
|
|
22
|
+
} finally {
|
|
23
|
+
this.running--
|
|
24
|
+
this.processQueue()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (this.running < this.maxConcurrency) {
|
|
29
|
+
runTask()
|
|
30
|
+
} else {
|
|
31
|
+
this.queue.push(runTask)
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private processQueue(): void {
|
|
37
|
+
if (this.queue.length > 0 && this.running < this.maxConcurrency) {
|
|
38
|
+
const nextTask = this.queue.shift()!
|
|
39
|
+
nextTask()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Process items in parallel with controlled concurrency
|
|
46
|
+
*/
|
|
47
|
+
export async function processInParallel<T, R>(
|
|
48
|
+
items: T[],
|
|
49
|
+
processor: (item: T, index: number) => Promise<R>,
|
|
50
|
+
maxConcurrency: number = 5
|
|
51
|
+
): Promise<R[]> {
|
|
52
|
+
const manager = new ConcurrencyManager(maxConcurrency)
|
|
53
|
+
const promises = items.map((item, index) =>
|
|
54
|
+
manager.execute(() => processor(item, index))
|
|
55
|
+
)
|
|
56
|
+
return Promise.all(promises)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Process items in batches with parallel processing within each batch
|
|
61
|
+
*/
|
|
62
|
+
export async function processInBatches<T, R>(
|
|
63
|
+
items: T[],
|
|
64
|
+
processor: (item: T, index: number) => Promise<R>,
|
|
65
|
+
batchSize: number = 10,
|
|
66
|
+
maxConcurrencyPerBatch: number = 5
|
|
67
|
+
): Promise<R[]> {
|
|
68
|
+
const results: R[] = []
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
71
|
+
const batch = items.slice(i, i + batchSize)
|
|
72
|
+
const batchResults = await processInParallel(
|
|
73
|
+
batch,
|
|
74
|
+
(item, batchIndex) => processor(item, i + batchIndex),
|
|
75
|
+
maxConcurrencyPerBatch
|
|
76
|
+
)
|
|
77
|
+
results.push(...batchResults)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return results
|
|
81
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import readline from "readline"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Common utility functions used across the application
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extracts error message from unknown error type
|
|
9
|
+
*/
|
|
10
|
+
export function getErrorMessage(error: unknown): string {
|
|
11
|
+
return error instanceof Error ? error.message : "Unknown error"
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates a promise-based readline interface for user input
|
|
16
|
+
*/
|
|
17
|
+
export function createPromiseReadline(): {
|
|
18
|
+
question: (prompt: string) => Promise<string>
|
|
19
|
+
close: () => void
|
|
20
|
+
} {
|
|
21
|
+
const rl = readline.createInterface({
|
|
22
|
+
input: process.stdin,
|
|
23
|
+
output: process.stdout,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
question: (prompt: string): Promise<string> => {
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
rl.question(prompt, (answer: string) => {
|
|
30
|
+
resolve(answer.trim())
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
},
|
|
34
|
+
close: () => rl.close(),
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Sleep utility for async delays
|
|
40
|
+
*/
|
|
41
|
+
export function sleep(ms: number): Promise<void> {
|
|
42
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Formats file size in human readable format
|
|
47
|
+
*/
|
|
48
|
+
export function formatFileSize(bytes: number): string {
|
|
49
|
+
const units = ["B", "KB", "MB", "GB"]
|
|
50
|
+
let size = bytes
|
|
51
|
+
let unitIndex = 0
|
|
52
|
+
|
|
53
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
54
|
+
size /= 1024
|
|
55
|
+
unitIndex++
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return `${size.toFixed(1)} ${units[unitIndex]}`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Truncates text to specified length with ellipsis
|
|
63
|
+
*/
|
|
64
|
+
export function truncateText(text: string, maxLength: number): string {
|
|
65
|
+
if (text.length <= maxLength) {
|
|
66
|
+
return text
|
|
67
|
+
}
|
|
68
|
+
return text.substring(0, maxLength - 3) + "..."
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Calculates percentage with proper rounding
|
|
73
|
+
*/
|
|
74
|
+
export function calculatePercentage(part: number, total: number): number {
|
|
75
|
+
if (total === 0) return 0
|
|
76
|
+
return Math.round((part / total) * 100)
|
|
77
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -12,7 +12,13 @@
|
|
|
12
12
|
"resolveJsonModule": true,
|
|
13
13
|
"declaration": true,
|
|
14
14
|
"declarationMap": true,
|
|
15
|
-
"sourceMap": true
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"paths": {
|
|
17
|
+
"@domain/*": ["./src/1.domain/*"],
|
|
18
|
+
"@app/*": ["./src/2.application/*"],
|
|
19
|
+
"@presentation/*": ["./src/3.presentation/*"],
|
|
20
|
+
"@infra/*": ["./src/4.infrastructure/*"]
|
|
21
|
+
}
|
|
16
22
|
},
|
|
17
23
|
"include": ["src/**/*"],
|
|
18
24
|
"exclude": ["node_modules", "dist"]
|
package/src/cli.ts
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
import { readFileSync, mkdirSync } from "fs"
|
|
2
|
-
import { Command } from "commander"
|
|
3
|
-
import { join, dirname } from "path"
|
|
4
|
-
|
|
5
|
-
export interface CLIOptions {
|
|
6
|
-
output?: string
|
|
7
|
-
outputDir?: string
|
|
8
|
-
file?: string
|
|
9
|
-
commits: string[]
|
|
10
|
-
author?: string
|
|
11
|
-
limit?: number
|
|
12
|
-
useDefaults: boolean
|
|
13
|
-
resume?: boolean
|
|
14
|
-
clear?: boolean
|
|
15
|
-
model?: string
|
|
16
|
-
report?: boolean
|
|
17
|
-
inputCsv?: string
|
|
18
|
-
verbose?: boolean
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export class CLIService {
|
|
22
|
-
static parseArguments(): CLIOptions {
|
|
23
|
-
const program = new Command()
|
|
24
|
-
|
|
25
|
-
program
|
|
26
|
-
.name("commit-analyzer")
|
|
27
|
-
.description(
|
|
28
|
-
"Analyze user authored git commits and generate rich commit descriptions and stakeholder reports from them.",
|
|
29
|
-
)
|
|
30
|
-
.version("1.0.3")
|
|
31
|
-
.option("-o, --output <file>", "Output CSV file (default: commits.csv)")
|
|
32
|
-
.option(
|
|
33
|
-
"--output-dir <dir>",
|
|
34
|
-
"Output directory for CSV and report files (default: current directory)",
|
|
35
|
-
)
|
|
36
|
-
.option(
|
|
37
|
-
"-f, --file <file>",
|
|
38
|
-
"Read commit hashes from file (one per line)",
|
|
39
|
-
)
|
|
40
|
-
.option(
|
|
41
|
-
"-a, --author <email>",
|
|
42
|
-
"Filter commits by author email (defaults to current user)",
|
|
43
|
-
)
|
|
44
|
-
.option(
|
|
45
|
-
"-l, --limit <number>",
|
|
46
|
-
"Limit number of commits to analyze",
|
|
47
|
-
parseInt,
|
|
48
|
-
)
|
|
49
|
-
.option("-r, --resume", "Resume from last checkpoint if available")
|
|
50
|
-
.option("-c, --clear", "Clear any existing progress checkpoint")
|
|
51
|
-
.option("-m, --model <model>", "LLM model to use (claude, gemini, codex)")
|
|
52
|
-
.option(
|
|
53
|
-
"--report",
|
|
54
|
-
"Generate condensed markdown report from existing CSV",
|
|
55
|
-
)
|
|
56
|
-
.option(
|
|
57
|
-
"--input-csv <file>",
|
|
58
|
-
"Input CSV file to read for report generation",
|
|
59
|
-
)
|
|
60
|
-
.option(
|
|
61
|
-
"-v, --verbose",
|
|
62
|
-
"Enable verbose logging (shows detailed error information)",
|
|
63
|
-
)
|
|
64
|
-
.argument(
|
|
65
|
-
"[commits...]",
|
|
66
|
-
"Commit hashes to analyze (if none provided, uses current user's commits)",
|
|
67
|
-
)
|
|
68
|
-
.parse()
|
|
69
|
-
|
|
70
|
-
const options = program.opts()
|
|
71
|
-
const args = program.args
|
|
72
|
-
|
|
73
|
-
let commits: string[] = []
|
|
74
|
-
let useDefaults = false
|
|
75
|
-
|
|
76
|
-
if (options.file) {
|
|
77
|
-
commits = this.readCommitsFromFile(options.file)
|
|
78
|
-
} else if (args.length > 0) {
|
|
79
|
-
commits = args
|
|
80
|
-
} else {
|
|
81
|
-
useDefaults = true
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return {
|
|
85
|
-
output:
|
|
86
|
-
options.output ||
|
|
87
|
-
CLIService.resolveOutputPath("commits.csv", options.outputDir),
|
|
88
|
-
outputDir: options.outputDir,
|
|
89
|
-
file: options.file,
|
|
90
|
-
commits,
|
|
91
|
-
author: options.author,
|
|
92
|
-
limit: options.limit,
|
|
93
|
-
useDefaults,
|
|
94
|
-
resume: options.resume,
|
|
95
|
-
clear: options.clear,
|
|
96
|
-
model: options.model,
|
|
97
|
-
report: options.report,
|
|
98
|
-
inputCsv: options.inputCsv,
|
|
99
|
-
verbose: options.verbose,
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
private static readCommitsFromFile(filename: string): string[] {
|
|
104
|
-
try {
|
|
105
|
-
const content = readFileSync(filename, "utf8")
|
|
106
|
-
return content
|
|
107
|
-
.split("\n")
|
|
108
|
-
.map((line) => line.trim())
|
|
109
|
-
.filter((line) => line.length > 0)
|
|
110
|
-
} catch (error) {
|
|
111
|
-
throw new Error(
|
|
112
|
-
`Failed to read commits from file ${filename}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
113
|
-
)
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Resolve the full file path with optional output directory.
|
|
119
|
-
*/
|
|
120
|
-
static resolveOutputPath(filename: string, outputDir?: string): string {
|
|
121
|
-
if (outputDir) {
|
|
122
|
-
// Ensure output directory exists
|
|
123
|
-
try {
|
|
124
|
-
mkdirSync(outputDir, { recursive: true })
|
|
125
|
-
} catch (error) {
|
|
126
|
-
throw new Error(
|
|
127
|
-
`Failed to create output directory ${outputDir}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
128
|
-
)
|
|
129
|
-
}
|
|
130
|
-
return join(outputDir, filename)
|
|
131
|
-
}
|
|
132
|
-
return filename
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
static showHelp(): void {
|
|
136
|
-
console.log(`
|
|
137
|
-
Usage: commit-analyzer [options] [commits...]
|
|
138
|
-
|
|
139
|
-
Analyze git commits and generate categorized summaries using LLM.
|
|
140
|
-
If no commits are specified, analyzes all commits authored by the current user.
|
|
141
|
-
|
|
142
|
-
Options:
|
|
143
|
-
-o, --output <file> Output file (default: commits.csv for analysis, report.md for reports)
|
|
144
|
-
--output-dir <dir> Output directory for CSV and report files (default: current directory)
|
|
145
|
-
-f, --file <file> Read commit hashes from file (one per line)
|
|
146
|
-
-a, --author <email> Filter commits by author email (defaults to current user)
|
|
147
|
-
-l, --limit <number> Limit number of commits to analyze
|
|
148
|
-
-r, --resume Resume from last checkpoint if available
|
|
149
|
-
-c, --clear Clear any existing progress checkpoint
|
|
150
|
-
--report Generate condensed markdown report from existing CSV
|
|
151
|
-
--input-csv <file> Input CSV file to read for report generation
|
|
152
|
-
-v, --verbose Enable verbose logging (shows detailed error information)
|
|
153
|
-
-h, --help Display help for command
|
|
154
|
-
-V, --version Display version number
|
|
155
|
-
|
|
156
|
-
Examples:
|
|
157
|
-
commit-analyzer # Analyze your authored commits
|
|
158
|
-
commit-analyzer --limit 10 # Analyze your last 10 commits
|
|
159
|
-
commit-analyzer --author user@example.com # Analyze specific user's commits
|
|
160
|
-
commit-analyzer abc123 def456 ghi789 # Analyze specific commits
|
|
161
|
-
commit-analyzer --file commits.txt # Read commits from file
|
|
162
|
-
commit-analyzer --output analysis.csv --limit 20 # Analyze last 20 commits to custom file
|
|
163
|
-
commit-analyzer --resume # Resume from last checkpoint
|
|
164
|
-
commit-analyzer --clear # Clear checkpoint and start fresh
|
|
165
|
-
commit-analyzer --report # Analyze commits, generate CSV, then generate report
|
|
166
|
-
commit-analyzer --input-csv data.csv --report # Skip analysis, generate report from existing CSV
|
|
167
|
-
commit-analyzer --report -o custom-report.md # Analyze commits, generate CSV, then generate custom report
|
|
168
|
-
`)
|
|
169
|
-
}
|
|
170
|
-
}
|
package/src/csv-reader.ts
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "fs"
|
|
2
|
-
|
|
3
|
-
export interface ParsedCSVRow {
|
|
4
|
-
year: number
|
|
5
|
-
category: "tweak" | "feature" | "process"
|
|
6
|
-
summary: string
|
|
7
|
-
description: string
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export class CSVReaderService {
|
|
11
|
-
static readCSV(filename: string): ParsedCSVRow[] {
|
|
12
|
-
try {
|
|
13
|
-
const content = readFileSync(filename, "utf8")
|
|
14
|
-
return this.parseCSV(content)
|
|
15
|
-
} catch (error) {
|
|
16
|
-
throw new Error(
|
|
17
|
-
`Failed to read CSV file ${filename}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
18
|
-
)
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
private static parseCSV(content: string): ParsedCSVRow[] {
|
|
23
|
-
const lines = content.trim().split("\n")
|
|
24
|
-
|
|
25
|
-
if (lines.length === 0) {
|
|
26
|
-
throw new Error("CSV file is empty")
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Validate header
|
|
30
|
-
const header = lines[0].toLowerCase()
|
|
31
|
-
const expectedHeader = "year,category,summary,description"
|
|
32
|
-
if (header !== expectedHeader) {
|
|
33
|
-
throw new Error(
|
|
34
|
-
`Invalid CSV format. Expected header: "${expectedHeader}", got: "${header}"`,
|
|
35
|
-
)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const rows: ParsedCSVRow[] = []
|
|
39
|
-
|
|
40
|
-
for (let i = 1; i < lines.length; i++) {
|
|
41
|
-
const line = lines[i].trim()
|
|
42
|
-
if (line === "") continue // Skip empty lines
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
const row = this.parseCSVLine(line, i + 1)
|
|
46
|
-
rows.push(row)
|
|
47
|
-
} catch (error) {
|
|
48
|
-
throw new Error(
|
|
49
|
-
`Error parsing CSV line ${i + 1}: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
50
|
-
)
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return rows
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
private static parseCSVLine(line: string, lineNumber: number): ParsedCSVRow {
|
|
58
|
-
const fields = this.parseCSVFields(line)
|
|
59
|
-
|
|
60
|
-
if (fields.length !== 4) {
|
|
61
|
-
throw new Error(
|
|
62
|
-
`Expected 4 fields (year,category,summary,description), got ${fields.length}`,
|
|
63
|
-
)
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const [yearStr, category, summary, description] = fields
|
|
67
|
-
|
|
68
|
-
// Validate year
|
|
69
|
-
const year = parseInt(yearStr, 10)
|
|
70
|
-
if (isNaN(year) || year < 1900 || year > 2100) {
|
|
71
|
-
throw new Error(`Invalid year: ${yearStr}`)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Validate category
|
|
75
|
-
if (!this.isValidCategory(category)) {
|
|
76
|
-
throw new Error(
|
|
77
|
-
`Invalid category: ${category}. Must be one of: tweak, feature, process`,
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Validate required fields
|
|
82
|
-
if (!summary.trim()) {
|
|
83
|
-
throw new Error("Summary field cannot be empty")
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (!description.trim()) {
|
|
87
|
-
throw new Error("Description field cannot be empty")
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
year,
|
|
92
|
-
category: category as "tweak" | "feature" | "process",
|
|
93
|
-
summary: summary.trim(),
|
|
94
|
-
description: description.trim(),
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Parse CSV fields handling quoted fields with commas and escaped quotes
|
|
100
|
-
*/
|
|
101
|
-
private static parseCSVFields(line: string): string[] {
|
|
102
|
-
const fields: string[] = []
|
|
103
|
-
let currentField = ""
|
|
104
|
-
let inQuotes = false
|
|
105
|
-
let i = 0
|
|
106
|
-
|
|
107
|
-
while (i < line.length) {
|
|
108
|
-
const char = line[i]
|
|
109
|
-
const nextChar = line[i + 1]
|
|
110
|
-
|
|
111
|
-
if (char === '"') {
|
|
112
|
-
if (inQuotes && nextChar === '"') {
|
|
113
|
-
// Escaped quote inside quoted field
|
|
114
|
-
currentField += '"'
|
|
115
|
-
i += 2
|
|
116
|
-
} else {
|
|
117
|
-
// Start or end of quoted field
|
|
118
|
-
inQuotes = !inQuotes
|
|
119
|
-
i++
|
|
120
|
-
}
|
|
121
|
-
} else if (char === "," && !inQuotes) {
|
|
122
|
-
// Field separator outside quotes
|
|
123
|
-
fields.push(currentField)
|
|
124
|
-
currentField = ""
|
|
125
|
-
i++
|
|
126
|
-
} else {
|
|
127
|
-
// Regular character
|
|
128
|
-
currentField += char
|
|
129
|
-
i++
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Add the last field
|
|
134
|
-
fields.push(currentField)
|
|
135
|
-
|
|
136
|
-
return fields
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
private static isValidCategory(
|
|
140
|
-
category: string,
|
|
141
|
-
): category is "tweak" | "feature" | "process" {
|
|
142
|
-
return ["tweak", "feature", "process"].includes(category)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
* Get summary statistics about the CSV data
|
|
147
|
-
*/
|
|
148
|
-
static getStatistics(rows: ParsedCSVRow[]): {
|
|
149
|
-
totalRows: number
|
|
150
|
-
yearRange: { min: number; max: number }
|
|
151
|
-
categoryBreakdown: Record<string, number>
|
|
152
|
-
} {
|
|
153
|
-
if (rows.length === 0) {
|
|
154
|
-
return {
|
|
155
|
-
totalRows: 0,
|
|
156
|
-
yearRange: { min: 0, max: 0 },
|
|
157
|
-
categoryBreakdown: { tweak: 0, feature: 0, process: 0 },
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const years = rows.map((row) => row.year)
|
|
162
|
-
const categoryBreakdown = rows.reduce(
|
|
163
|
-
(acc, row) => {
|
|
164
|
-
acc[row.category]++
|
|
165
|
-
return acc
|
|
166
|
-
},
|
|
167
|
-
{ tweak: 0, feature: 0, process: 0 } as Record<string, number>,
|
|
168
|
-
)
|
|
169
|
-
|
|
170
|
-
return {
|
|
171
|
-
totalRows: rows.length,
|
|
172
|
-
yearRange: {
|
|
173
|
-
min: Math.min(...years),
|
|
174
|
-
max: Math.max(...years),
|
|
175
|
-
},
|
|
176
|
-
categoryBreakdown,
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
package/src/csv.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { writeFileSync } from "fs"
|
|
2
|
-
import { AnalyzedCommit, CSVRow } from "./types"
|
|
3
|
-
|
|
4
|
-
export class CSVService {
|
|
5
|
-
static generateCSV(commits: AnalyzedCommit[]): string {
|
|
6
|
-
const headers = "year,category,summary,description"
|
|
7
|
-
const rows = commits.map((commit) => this.formatRow(commit))
|
|
8
|
-
|
|
9
|
-
return [headers, ...rows].join("\n")
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
static exportToFile(commits: AnalyzedCommit[], filename: string): void {
|
|
13
|
-
const csvContent = this.generateCSV(commits)
|
|
14
|
-
writeFileSync(filename, csvContent, "utf8")
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
private static formatRow(commit: AnalyzedCommit): string {
|
|
18
|
-
const row: CSVRow = {
|
|
19
|
-
year: commit.year,
|
|
20
|
-
category: commit.analysis.category,
|
|
21
|
-
summary: commit.analysis.summary,
|
|
22
|
-
description: commit.analysis.description,
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return [
|
|
26
|
-
row.year,
|
|
27
|
-
this.escapeCsvField(row.category),
|
|
28
|
-
this.escapeCsvField(row.summary),
|
|
29
|
-
this.escapeCsvField(row.description),
|
|
30
|
-
].join(",")
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
private static escapeCsvField(field: string): string {
|
|
34
|
-
if (field.includes(",") || field.includes('"') || field.includes("\n")) {
|
|
35
|
-
return `"${field.replace(/"/g, '""')}"`
|
|
36
|
-
}
|
|
37
|
-
return field
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|